//! Handler for the `show` command. use super::CommandContext; use crate::io::story_metadata::QaMode; /// Strip YAML front matter and return a summary of useful fields + the remaining body. #[allow(clippy::string_slice)] // indices from find("\n---") on ASCII delimiter; "---" and "\n---" are ASCII-only fn strip_front_matter(text: &str) -> (String, String) { let trimmed = text.trim_start(); if !trimmed.starts_with("---") { return (String::new(), text.to_string()); } // Find the closing --- if let Some(end) = trimmed[3..].find("\n---") { let yaml_block = &trimmed[3..3 + end].trim(); let body = &trimmed[3 + end + 4..]; // skip past closing --- // Extract useful fields from YAML (simple line-based parsing) let mut parts = Vec::new(); for line in yaml_block.lines() { let line = line.trim(); if line.starts_with("depends_on:") { let val = line.trim_start_matches("depends_on:").trim(); if !val.is_empty() && val != "[]" { parts.push(format!("**Depends on:** {val}")); } } else if line.starts_with("agent:") { let val = line.trim_start_matches("agent:").trim().trim_matches('"'); if !val.is_empty() { parts.push(format!("**Agent:** {val}")); } } else if line.starts_with("blocked:") { let val = line.trim_start_matches("blocked:").trim(); if val == "true" { parts.push("**Blocked:** yes".to_string()); } } else if line.starts_with("retry_count:") { let val = line.trim_start_matches("retry_count:").trim(); if val != "0" && !val.is_empty() { parts.push(format!("**Retries:** {val}")); } } else if line.starts_with("qa:") { let val = line.trim_start_matches("qa:").trim().trim_matches('"'); if let Some(QaMode::Human) = QaMode::from_str(val) { parts.push("**QA:** human review required".to_string()); } } else if line.starts_with("merge_failure:") { let val = line .trim_start_matches("merge_failure:") .trim() .trim_matches('"'); if !val.is_empty() { parts.push(format!("**Merge failure:** {val}")); } } } (parts.join(" ยท "), body.to_string()) } else { // No closing ---, return as-is (String::new(), text.to_string()) } } /// Display the full markdown text of a work item identified by its numeric ID. /// /// Lookup priority: CRDT โ†’ content store โ†’ filesystem (Story 512). /// Returns a friendly message when no match is found. pub(super) fn handle_show(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); if num_str.is_empty() { return Some(format!( "Usage: `{} show `\n\nDisplays the full text of a story, bug, or spike.", ctx.services.bot_name )); } if !num_str.chars().all(|c| c.is_ascii_digit()) { return Some(format!( "Invalid story number: `{num_str}`. Usage: `{} show `", ctx.services.bot_name )); } // Find the story by numeric prefix: CRDT โ†’ content store. let (story_id, _stage_dir, _path, content) = match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) { Some(found) => found, None => { return Some(format!( "No story, bug, or spike with number **{num_str}** found." )); } }; // `content` comes from the CRDT / content store. If unavailable, report // it rather than silently reading a stale on-disk copy. let text = content.unwrap_or_else(|| { format!("Story {story_id} found in pipeline but its content is unavailable.") }); // Strip front matter block and extract useful metadata to show inline. let (front_matter_summary, body) = strip_front_matter(&text); // Convert markdown headings to bold text for consistent rendering across // Matrix clients. Element X doesn't style

tags distinctly, but bold // text renders consistently everywhere. let formatted = body .lines() .map(|line| { let trimmed = line.trim_start(); if let Some(rest) = trimmed.strip_prefix("### ") { format!("\n**{}**", rest) } else if let Some(rest) = trimmed.strip_prefix("## ") { format!("\n**{}**", rest) } else if let Some(rest) = trimmed.strip_prefix("# ") { format!("\n**{}**", rest) } else { line.to_string() } }) .collect::>() .join("\n"); if front_matter_summary.is_empty() { Some(formatted.trim().to_string()) } else { Some(format!("{front_matter_summary}\n{}", formatted.trim())) } } #[cfg(test)] mod tests { use super::super::{CommandDispatch, try_handle_command}; fn show_cmd_with_root(root: &std::path::Path, args: &str) -> Option { let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string()); let room_id = "!test:example.com".to_string(); let dispatch = CommandDispatch { services: &services, project_root: &services.project_root, bot_user_id: "@timmy:homeserver.local", room_id: &room_id, }; try_handle_command(&dispatch, &format!("@timmy show {args}")) } use crate::chat::test_helpers::write_story_file; #[test] fn show_command_is_registered() { use super::super::commands; let found = commands().iter().any(|c| c.name == "show"); assert!(found, "show command must be in the registry"); } #[test] fn show_command_appears_in_help() { let result = super::super::tests::try_cmd_addressed( "Timmy", "@timmy:homeserver.local", "@timmy help", ); let output = result.unwrap(); assert!( output.contains("show"), "help should list show command: {output}" ); } #[test] fn show_command_no_args_returns_usage() { let tmp = tempfile::TempDir::new().unwrap(); let output = show_cmd_with_root(tmp.path(), "").unwrap(); assert!( output.contains("Usage"), "no args should show usage hint: {output}" ); } #[test] fn show_command_non_numeric_args_returns_error() { let tmp = tempfile::TempDir::new().unwrap(); let output = show_cmd_with_root(tmp.path(), "abc").unwrap(); assert!( output.contains("Invalid"), "non-numeric arg should return error message: {output}" ); } #[test] fn show_command_not_found_returns_friendly_message() { let tmp = tempfile::TempDir::new().unwrap(); let output = show_cmd_with_root(tmp.path(), "999").unwrap(); assert!( output.contains("999"), "not-found message should include the queried number: {output}" ); assert!( output.contains("found"), "not-found message should say not found: {output}" ); } #[test] fn show_command_finds_story_in_backlog() { let tmp = tempfile::TempDir::new().unwrap(); write_story_file( tmp.path(), "1_backlog", "305_story_show_command.md", "---\nname: Show command\n---\n\n# Story 305\n\nFull story text here.", None, ); let output = show_cmd_with_root(tmp.path(), "305").unwrap(); assert!( output.contains("Full story text here."), "show should return full story content: {output}" ); } #[test] fn show_command_finds_story_in_current() { let tmp = tempfile::TempDir::new().unwrap(); write_story_file( tmp.path(), "2_current", "42_story_do_something.md", "---\nname: Do something\n---\n\n# Story 42\n\nIn progress.", None, ); let output = show_cmd_with_root(tmp.path(), "42").unwrap(); assert!( output.contains("In progress."), "show should return story from current stage: {output}" ); } #[test] fn show_command_finds_bug() { let tmp = tempfile::TempDir::new().unwrap(); write_story_file( tmp.path(), "1_backlog", "7_bug_crash_on_login.md", "---\nname: Crash on login\n---\n\n## Symptom\n\nCrashes.", None, ); let output = show_cmd_with_root(tmp.path(), "7").unwrap(); assert!( output.contains("Symptom"), "show should return bug content: {output}" ); } #[test] fn show_command_case_insensitive() { let result = super::super::tests::try_cmd_addressed( "Timmy", "@timmy:homeserver.local", "@timmy SHOW 1", ); assert!(result.is_some(), "SHOW should match case-insensitively"); } }