zeph_commands/handlers/
help.rs1use std::fmt::Write as _;
7use std::future::Future;
8use std::pin::Pin;
9
10use crate::context::CommandContext;
11use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
12
13#[must_use]
21pub fn render_help_text() -> String {
22 let mut out = String::from("Slash commands:\n\n");
23
24 let categories = [
25 SlashCategory::Session,
26 SlashCategory::Configuration,
27 SlashCategory::Memory,
28 SlashCategory::Skills,
29 SlashCategory::Planning,
30 SlashCategory::Integration,
31 SlashCategory::Debugging,
32 SlashCategory::Advanced,
33 ];
34
35 for cat in &categories {
36 let entries: Vec<_> = crate::COMMANDS
37 .iter()
38 .filter(|c| &c.category == cat)
39 .collect();
40 if entries.is_empty() {
41 continue;
42 }
43 let _ = writeln!(out, "{}:", cat.as_str());
44 for cmd in entries {
45 if cmd.args.is_empty() {
46 let _ = write!(out, " {}", cmd.name);
47 } else {
48 let _ = write!(out, " {} {}", cmd.name, cmd.args);
49 }
50 let _ = write!(out, " — {}", cmd.description);
51 if let Some(feat) = cmd.feature_gate {
52 let _ = write!(out, " [requires: {feat}]");
53 }
54 let _ = writeln!(out);
55 }
56 let _ = writeln!(out);
57 }
58
59 out.trim_end().to_owned()
60}
61
62pub struct HelpCommand;
64
65impl CommandHandler<CommandContext<'_>> for HelpCommand {
66 fn name(&self) -> &'static str {
67 "/help"
68 }
69
70 fn description(&self) -> &'static str {
71 "Show this help message"
72 }
73
74 fn category(&self) -> SlashCategory {
75 SlashCategory::Debugging
76 }
77
78 fn requires_auth(&self) -> bool {
79 false
80 }
81
82 fn handle<'a>(
83 &'a self,
84 _ctx: &'a mut CommandContext<'_>,
85 _args: &'a str,
86 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
87 use tracing::Instrument as _;
88 let span = tracing::info_span!("commands.help.handle");
89 Box::pin(async move { Ok(CommandOutput::Message(render_help_text())) }.instrument(span))
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
97 use crate::sink::NullSink;
98
99 #[test]
100 fn help_name_and_description() {
101 assert_eq!(HelpCommand.name(), "/help");
102 assert!(!HelpCommand.description().is_empty());
103 }
104
105 #[test]
106 fn help_requires_auth_false() {
107 assert!(!HelpCommand.requires_auth());
108 }
109
110 #[tokio::test]
111 async fn help_returns_message_with_slash_commands_header() {
112 let mut sink = NullSink;
113 let mut debug = MockDebug;
114 let mut messages = MockMessages;
115 let session = MockSession;
116 let mut agent = crate::NullAgent;
117 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
118 let out = HelpCommand.handle(&mut ctx, "").await.unwrap();
119 let CommandOutput::Message(msg) = out else {
120 panic!("expected Message")
121 };
122 assert!(msg.contains("Slash commands"));
123 }
124}