zeph_commands/handlers/
agent_cmd.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct AgentCommand;
18
19impl CommandHandler<CommandContext<'_>> for AgentCommand {
20 fn name(&self) -> &'static str {
21 "/agent"
22 }
23
24 fn description(&self) -> &'static str {
25 "Manage sub-agents"
26 }
27
28 fn args_hint(&self) -> &'static str {
29 "[subcommand]"
30 }
31
32 fn category(&self) -> SlashCategory {
33 SlashCategory::Integration
34 }
35
36 fn requires_auth(&self) -> bool {
37 true
38 }
39
40 fn handle<'a>(
41 &'a self,
42 ctx: &'a mut CommandContext<'_>,
43 args: &'a str,
44 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
45 use tracing::Instrument as _;
46 let span = tracing::info_span!("commands.agent.handle");
47 Box::pin(
48 async move {
49 let input = if args.is_empty() {
50 "/agent".to_owned()
51 } else {
52 format!("/agent {args}")
53 };
54 match ctx.agent.handle_agent_dispatch(&input).await? {
55 Some(msg) => Ok(CommandOutput::Message(msg)),
56 None => Ok(CommandOutput::Silent),
57 }
58 }
59 .instrument(span),
60 )
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::CommandRegistry;
68 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
69 use crate::sink::NullSink;
70 use std::assert_matches;
71
72 #[test]
73 fn agent_cmd_name_and_description() {
74 assert_eq!(AgentCommand.name(), "/agent");
75 assert!(!AgentCommand.description().is_empty());
76 }
77
78 #[tokio::test]
79 async fn agent_dispatch_none_returns_silent() {
80 let mut sink = NullSink;
82 let mut debug = MockDebug;
83 let mut messages = MockMessages;
84 let session = MockSession;
85 let mut agent = crate::NullAgent;
86 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
87 let out = AgentCommand.handle(&mut ctx, "").await.unwrap();
88 assert_matches!(out, CommandOutput::Silent);
89 }
90
91 #[tokio::test]
92 async fn agent_dispatch_with_args_returns_silent() {
93 let mut sink = NullSink;
94 let mut debug = MockDebug;
95 let mut messages = MockMessages;
96 let session = MockSession;
97 let mut agent = crate::NullAgent;
98 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
99 let out = AgentCommand.handle(&mut ctx, "list").await.unwrap();
100 assert_matches!(out, CommandOutput::Silent);
101 }
102
103 #[tokio::test]
104 async fn agent_dispatch_allowed_when_trusted() {
105 let mut sink = NullSink;
106 let mut debug = MockDebug;
107 let mut messages = MockMessages;
108 let session = MockSession;
109 let mut agent = crate::NullAgent;
110 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
111
112 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
113 reg.register(AgentCommand);
114
115 let result = reg.dispatch(&mut ctx, "/agent list", true).await;
116 assert!(result.unwrap().is_ok());
117 }
118
119 #[tokio::test]
120 async fn agent_dispatch_rejected_when_untrusted() {
121 let mut sink = NullSink;
122 let mut debug = MockDebug;
123 let mut messages = MockMessages;
124 let session = MockSession;
125 let mut agent = crate::NullAgent;
126 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
127
128 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
129 reg.register(AgentCommand);
130
131 let result = reg.dispatch(&mut ctx, "/agent list", false).await;
132 let err = result.unwrap().unwrap_err();
133 assert!(err.0.contains("trusted"));
134 }
135}