//! Handler for the `show` command. use super::CommandContext; /// Display the full markdown text of a work item identified by its numeric ID. /// /// Searches all pipeline stages in order and returns the raw file contents of /// the first matching story, bug, or spike. 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.bot_name )); } if !num_str.chars().all(|c| c.is_ascii_digit()) { return Some(format!( "Invalid story number: `{num_str}`. Usage: `{} show `", ctx.bot_name )); } let stages = [ "1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived", ]; for stage in &stages { let dir = ctx .project_root .join(".story_kit") .join("work") .join(stage); if !dir.exists() { continue; } if let Ok(entries) = std::fs::read_dir(&dir) { for entry in entries.flatten() { let path = entry.path(); if path.extension().and_then(|e| e.to_str()) != Some("md") { continue; } if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { let file_num = stem .split('_') .next() .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())) .unwrap_or(""); if file_num == num_str { return match std::fs::read_to_string(&path) { Ok(contents) => Some(contents), Err(e) => Some(format!("Failed to read story {num_str}: {e}")), }; } } } } } Some(format!( "No story, bug, or spike with number **{num_str}** found." )) } #[cfg(test)] mod tests { use crate::agents::AgentPool; use std::collections::HashSet; use std::sync::{Arc, Mutex}; use super::super::{CommandDispatch, try_handle_command}; fn show_cmd_with_root(root: &std::path::Path, args: &str) -> Option { let agents = Arc::new(AgentPool::new_test(3000)); let ambient_rooms = Arc::new(Mutex::new(HashSet::new())); let room_id = "!test:example.com".to_string(); let dispatch = CommandDispatch { bot_name: "Timmy", bot_user_id: "@timmy:homeserver.local", project_root: root, agents: &agents, ambient_rooms: &ambient_rooms, room_id: &room_id, }; try_handle_command(&dispatch, &format!("@timmy show {args}")) } fn write_story_file(root: &std::path::Path, stage: &str, filename: &str, content: &str) { let dir = root.join(".story_kit/work").join(stage); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join(filename), content).unwrap(); } #[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.", ); 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.", ); 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.", ); 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"); } }