//! Handler for the `help` command. use super::{commands, CommandContext}; pub(super) fn handle_help(ctx: &CommandContext) -> Option { let mut output = format!("**{} Commands**\n\n", ctx.bot_name); let mut sorted: Vec<_> = commands().iter().collect(); sorted.sort_by_key(|c| c.name); for cmd in sorted { output.push_str(&format!("- **{}** — {}\n", cmd.name, cmd.description)); } Some(output) } #[cfg(test)] mod tests { use super::super::tests::{try_cmd_addressed, commands}; #[test] fn help_command_matches() { let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help"); assert!(result.is_some(), "help command should match"); } #[test] fn help_command_case_insensitive() { let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy HELP"); assert!(result.is_some(), "HELP should match case-insensitively"); } #[test] fn help_output_contains_all_commands() { let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help"); let output = result.unwrap(); for cmd in commands() { assert!( output.contains(cmd.name), "help output must include command '{}'", cmd.name ); assert!( output.contains(cmd.description), "help output must include description for '{}'", cmd.name ); } } #[test] fn help_output_uses_bot_name() { let result = try_cmd_addressed("HAL", "@hal:example.com", "@hal help"); let output = result.unwrap(); assert!( output.contains("HAL Commands"), "help output should use bot name: {output}" ); } #[test] fn help_output_formatted_as_markdown() { let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help"); let output = result.unwrap(); assert!( output.contains("**help**"), "command name should be bold: {output}" ); assert!( output.contains("- **"), "commands should be in a list: {output}" ); } #[test] fn help_output_includes_status() { let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help"); let output = result.unwrap(); assert!(output.contains("status"), "help should list status command: {output}"); } #[test] fn help_output_is_alphabetical() { let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help"); let output = result.unwrap(); // Search for **name** (bold markdown) to avoid substring matches in descriptions. let mut positions: Vec<(usize, &str)> = commands() .iter() .map(|c| { let marker = format!("**{}**", c.name); let pos = output.find(&marker).expect("command must appear in help as **name**"); (pos, c.name) }) .collect(); positions.sort_by_key(|(pos, _)| *pos); let names_in_order: Vec<&str> = positions.iter().map(|(_, n)| *n).collect(); let mut sorted = names_in_order.clone(); sorted.sort(); assert_eq!(names_in_order, sorted, "commands must appear in alphabetical order"); } #[test] fn help_output_includes_ambient() { let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help"); let output = result.unwrap(); assert!(output.contains("ambient"), "help should list ambient command: {output}"); } #[test] fn help_output_includes_htop() { let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help"); let output = result.unwrap(); assert!(output.contains("htop"), "help should list htop command: {output}"); } }