zeph_commands/handlers/
misc.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct CacheStatsCommand;
14
15impl CommandHandler<CommandContext<'_>> for CacheStatsCommand {
16 fn name(&self) -> &'static str {
17 "/cache-stats"
18 }
19
20 fn description(&self) -> &'static str {
21 "Show tool orchestrator cache statistics"
22 }
23
24 fn category(&self) -> SlashCategory {
25 SlashCategory::Debugging
26 }
27
28 fn requires_auth(&self) -> bool {
29 true
30 }
31
32 fn handle<'a>(
33 &'a self,
34 ctx: &'a mut CommandContext<'_>,
35 _args: &'a str,
36 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
37 use tracing::Instrument as _;
38 let span = tracing::info_span!("commands.cache_stats.handle");
39 Box::pin(
40 async move {
41 let result = ctx.agent.cache_stats();
42 Ok(CommandOutput::Message(result))
43 }
44 .instrument(span),
45 )
46 }
47}
48
49pub struct NotifyTestCommand;
51
52impl CommandHandler<CommandContext<'_>> for NotifyTestCommand {
53 fn name(&self) -> &'static str {
54 "/notify-test"
55 }
56
57 fn description(&self) -> &'static str {
58 "Send a test notification via all enabled channels (macOS, webhook)"
59 }
60
61 fn category(&self) -> SlashCategory {
62 SlashCategory::Debugging
63 }
64
65 fn requires_auth(&self) -> bool {
66 true
67 }
68
69 fn handle<'a>(
70 &'a self,
71 ctx: &'a mut CommandContext<'_>,
72 _args: &'a str,
73 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
74 use tracing::Instrument as _;
75 let span = tracing::info_span!("commands.notify_test.handle");
76 Box::pin(
77 async move {
78 let result = ctx.agent.notify_test().await?;
79 Ok(CommandOutput::Message(result))
80 }
81 .instrument(span),
82 )
83 }
84}
85
86pub struct ImageCommand;
91
92impl CommandHandler<CommandContext<'_>> for ImageCommand {
93 fn name(&self) -> &'static str {
94 "/image"
95 }
96
97 fn description(&self) -> &'static str {
98 "Attach an image to the next message"
99 }
100
101 fn args_hint(&self) -> &'static str {
102 "<path>"
103 }
104
105 fn category(&self) -> SlashCategory {
106 SlashCategory::Integration
107 }
108
109 fn requires_auth(&self) -> bool {
110 true
111 }
112
113 fn handle<'a>(
114 &'a self,
115 ctx: &'a mut CommandContext<'_>,
116 args: &'a str,
117 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
118 use tracing::Instrument as _;
119 let span = tracing::info_span!("commands.image.handle");
120 Box::pin(
121 async move {
122 if args.is_empty() {
123 return Err(CommandError::new("Usage: /image <path>"));
124 }
125 let result = ctx.agent.load_image(args).await?;
126 Ok(CommandOutput::Message(result))
127 }
128 .instrument(span),
129 )
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::CommandRegistry;
137 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
138 use crate::sink::NullSink;
139 use std::assert_matches;
140
141 #[test]
142 fn cache_stats_name_and_description() {
143 assert_eq!(CacheStatsCommand.name(), "/cache-stats");
144 assert!(!CacheStatsCommand.description().is_empty());
145 }
146
147 #[test]
148 fn notify_test_name_and_description() {
149 assert_eq!(NotifyTestCommand.name(), "/notify-test");
150 assert!(!NotifyTestCommand.description().is_empty());
151 }
152
153 #[test]
154 fn image_name_and_description() {
155 assert_eq!(ImageCommand.name(), "/image");
156 assert!(!ImageCommand.description().is_empty());
157 }
158
159 #[tokio::test]
160 async fn cache_stats_returns_message() {
161 let mut sink = NullSink;
162 let mut debug = MockDebug;
163 let mut messages = MockMessages;
164 let session = MockSession;
165 let mut agent = crate::NullAgent;
166 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
167 let out = CacheStatsCommand.handle(&mut ctx, "").await.unwrap();
168 assert_matches!(out, CommandOutput::Message(_));
169 }
170
171 #[tokio::test]
172 async fn notify_test_returns_message() {
173 let mut sink = NullSink;
174 let mut debug = MockDebug;
175 let mut messages = MockMessages;
176 let session = MockSession;
177 let mut agent = crate::NullAgent;
178 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
179 let out = NotifyTestCommand.handle(&mut ctx, "").await.unwrap();
180 assert_matches!(out, CommandOutput::Message(_));
181 }
182
183 #[tokio::test]
184 async fn image_no_args_returns_error() {
185 let mut sink = NullSink;
186 let mut debug = MockDebug;
187 let mut messages = MockMessages;
188 let session = MockSession;
189 let mut agent = crate::NullAgent;
190 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
191 let err = ImageCommand.handle(&mut ctx, "").await.unwrap_err();
192 assert!(err.to_string().contains("/image"));
193 }
194
195 #[tokio::test]
196 async fn image_with_path_returns_message() {
197 let mut sink = NullSink;
198 let mut debug = MockDebug;
199 let mut messages = MockMessages;
200 let session = MockSession;
201 let mut agent = crate::NullAgent;
202 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
203 let out = ImageCommand
204 .handle(&mut ctx, "/tmp/photo.png")
205 .await
206 .unwrap();
207 assert_matches!(out, CommandOutput::Message(_));
208 }
209
210 #[tokio::test]
211 async fn image_dispatch_allowed_when_trusted() {
212 let mut sink = NullSink;
213 let mut debug = MockDebug;
214 let mut messages = MockMessages;
215 let session = MockSession;
216 let mut agent = crate::NullAgent;
217 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
218
219 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
220 reg.register(ImageCommand);
221
222 let result = reg.dispatch(&mut ctx, "/image photo.png", true).await;
223 assert!(result.unwrap().is_ok());
224 }
225
226 #[tokio::test]
227 async fn image_dispatch_rejected_when_untrusted() {
228 let mut sink = NullSink;
229 let mut debug = MockDebug;
230 let mut messages = MockMessages;
231 let session = MockSession;
232 let mut agent = crate::NullAgent;
233 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
234
235 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
236 reg.register(ImageCommand);
237
238 let result = reg.dispatch(&mut ctx, "/image photo.png", false).await;
239 let err = result.unwrap().unwrap_err();
240 assert!(err.0.contains("trusted"));
241 }
242}