zeph_commands/handlers/
debug.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::CommandHandler;
10use crate::context::CommandContext;
11use crate::{CommandError, CommandOutput, SlashCategory};
12
13pub struct LogCommand;
15
16impl CommandHandler<CommandContext<'_>> for LogCommand {
17 fn name(&self) -> &'static str {
18 "/log"
19 }
20
21 fn description(&self) -> &'static str {
22 "Toggle verbose log output"
23 }
24
25 fn category(&self) -> SlashCategory {
26 SlashCategory::Debugging
27 }
28
29 fn handle<'a>(
30 &'a self,
31 ctx: &'a mut CommandContext<'_>,
32 _args: &'a str,
33 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
34 Box::pin(async move {
35 let mut out = ctx.debug.log_status();
36 if let Some(tail) = ctx.debug.read_log_tail(20).await {
37 out.push('\n');
38 out.push_str("Recent entries:\n");
39 out.push_str(&ctx.debug.scrub(&tail));
40 }
41 Ok(CommandOutput::Message(out.trim_end().to_owned()))
42 })
43 }
44}
45
46pub struct DebugDumpCommand;
51
52impl CommandHandler<CommandContext<'_>> for DebugDumpCommand {
53 fn name(&self) -> &'static str {
54 "/debug-dump"
55 }
56
57 fn description(&self) -> &'static str {
58 "Enable or toggle debug dump output"
59 }
60
61 fn args_hint(&self) -> &'static str {
62 "[path]"
63 }
64
65 fn category(&self) -> SlashCategory {
66 SlashCategory::Debugging
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 Box::pin(async move {
75 if args.is_empty() {
76 let msg = match ctx.debug.dump_status() {
77 Some(path) => format!("Debug dump active: {path}"),
78 None => "Debug dump is inactive. Use `/debug-dump <path>` to enable, \
79 or start with `--debug-dump [dir]`."
80 .to_owned(),
81 };
82 return Ok(CommandOutput::Message(msg));
83 }
84
85 match ctx.debug.enable_dump(args) {
86 Ok(path) => Ok(CommandOutput::Message(format!(
87 "Debug dump enabled: {path}"
88 ))),
89 Err(e) => Ok(CommandOutput::Message(format!(
90 "Failed to enable debug dump: {e}"
91 ))),
92 }
93 })
94 }
95}
96
97pub struct DumpFormatCommand;
99
100impl CommandHandler<CommandContext<'_>> for DumpFormatCommand {
101 fn name(&self) -> &'static str {
102 "/dump-format"
103 }
104
105 fn description(&self) -> &'static str {
106 "Switch debug dump format at runtime"
107 }
108
109 fn args_hint(&self) -> &'static str {
110 "<json|raw|trace>"
111 }
112
113 fn category(&self) -> SlashCategory {
114 SlashCategory::Debugging
115 }
116
117 fn handle<'a>(
118 &'a self,
119 ctx: &'a mut CommandContext<'_>,
120 args: &'a str,
121 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
122 Box::pin(async move {
123 if args.is_empty() {
124 return Ok(CommandOutput::Message(format!(
125 "Current dump format: {}. Use `/dump-format json|raw|trace` to change.",
126 ctx.debug.dump_format_name()
127 )));
128 }
129
130 match ctx.debug.set_dump_format(args) {
131 Ok(()) => Ok(CommandOutput::Message(format!(
132 "Debug dump format set to: {args}"
133 ))),
134 Err(e) => Ok(CommandOutput::Message(e.to_string())),
135 }
136 })
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::CommandRegistry;
144 use crate::context::CommandContext;
145 use crate::sink::ChannelSink;
146 use crate::traits::debug::DebugAccess;
147 use crate::traits::messages::MessageAccess;
148 use crate::traits::session::SessionAccess;
149 use std::future::Future;
150 use std::pin::Pin;
151
152 struct MockSession;
153
154 impl SessionAccess for MockSession {
155 fn supports_exit(&self) -> bool {
156 false
157 }
158 }
159
160 struct MockSink;
161
162 impl ChannelSink for MockSink {
163 fn send<'a>(
164 &'a mut self,
165 _msg: &'a str,
166 ) -> Pin<Box<dyn Future<Output = Result<(), CommandError>> + Send + 'a>> {
167 Box::pin(async { Ok(()) })
168 }
169
170 fn flush_chunks<'a>(
171 &'a mut self,
172 ) -> Pin<Box<dyn Future<Output = Result<(), CommandError>> + Send + 'a>> {
173 Box::pin(async { Ok(()) })
174 }
175
176 fn send_queue_count<'a>(
177 &'a mut self,
178 _count: usize,
179 ) -> Pin<Box<dyn Future<Output = Result<(), CommandError>> + Send + 'a>> {
180 Box::pin(async { Ok(()) })
181 }
182
183 fn supports_exit(&self) -> bool {
184 false
185 }
186 }
187
188 struct MockDebug {
189 dump_active: bool,
190 format: String,
191 enable_result: Result<String, String>,
192 set_format_result: Result<(), String>,
193 }
194
195 impl MockDebug {
196 fn ok() -> Self {
197 Self {
198 dump_active: false,
199 format: "raw".to_owned(),
200 enable_result: Ok("/tmp/dump".to_owned()),
201 set_format_result: Ok(()),
202 }
203 }
204 }
205
206 impl DebugAccess for MockDebug {
207 fn log_status(&self) -> String {
208 "Log file: <disabled>\n".to_owned()
209 }
210
211 fn read_log_tail<'a>(
212 &'a self,
213 _n: usize,
214 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>> {
215 Box::pin(async { None })
216 }
217
218 fn scrub(&self, text: &str) -> String {
219 text.to_owned()
220 }
221
222 fn dump_status(&self) -> Option<String> {
223 if self.dump_active {
224 Some("/tmp/dump".to_owned())
225 } else {
226 None
227 }
228 }
229
230 fn dump_format_name(&self) -> String {
231 self.format.clone()
232 }
233
234 fn enable_dump(&mut self, _dir: &str) -> Result<String, CommandError> {
235 self.enable_result.clone().map_err(CommandError::new)
236 }
237
238 fn set_dump_format(&mut self, _name: &str) -> Result<(), CommandError> {
239 self.set_format_result.clone().map_err(CommandError::new)
240 }
241 }
242
243 struct MockMessages;
244
245 impl MessageAccess for MockMessages {
246 fn clear_history(&mut self) {}
247
248 fn queue_len(&self) -> usize {
249 0
250 }
251
252 fn drain_queue(&mut self) -> usize {
253 0
254 }
255
256 fn notify_queue_count<'a>(
257 &'a mut self,
258 _count: usize,
259 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
260 Box::pin(async {})
261 }
262 }
263
264 fn make_ctx<'a>(
265 sink: &'a mut MockSink,
266 debug: &'a mut MockDebug,
267 messages: &'a mut MockMessages,
268 session: &'a MockSession,
269 agent: &'a mut crate::NullAgent,
270 ) -> CommandContext<'a> {
271 CommandContext {
272 sink,
273 debug,
274 messages,
275 session: session as &dyn SessionAccess,
276 agent,
277 }
278 }
279
280 #[tokio::test]
281 async fn log_command_formats_status() {
282 let mut sink = MockSink;
283 let mut debug = MockDebug::ok();
284 let mut messages = MockMessages;
285 let session = MockSession;
286 let mut agent = crate::NullAgent;
287 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
288 let out = LogCommand.handle(&mut ctx, "").await.unwrap();
289 let CommandOutput::Message(msg) = out else {
290 panic!("expected Message")
291 };
292 assert!(msg.contains("<disabled>"));
293 }
294
295 #[tokio::test]
296 async fn debug_dump_no_args_reports_inactive() {
297 let mut sink = MockSink;
298 let mut debug = MockDebug::ok();
299 let mut messages = MockMessages;
300 let session = MockSession;
301 let mut agent = crate::NullAgent;
302 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
303 let out = DebugDumpCommand.handle(&mut ctx, "").await.unwrap();
304 let CommandOutput::Message(msg) = out else {
305 panic!("expected Message")
306 };
307 assert!(msg.contains("inactive"));
308 }
309
310 #[tokio::test]
311 async fn debug_dump_with_path_enables_dump() {
312 let mut sink = MockSink;
313 let mut debug = MockDebug::ok();
314 let mut messages = MockMessages;
315 let session = MockSession;
316 let mut agent = crate::NullAgent;
317 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
318 let out = DebugDumpCommand
319 .handle(&mut ctx, "/tmp/dump")
320 .await
321 .unwrap();
322 let CommandOutput::Message(msg) = out else {
323 panic!("expected Message")
324 };
325 assert!(msg.contains("enabled"));
326 }
327
328 #[tokio::test]
329 async fn dump_format_no_args_shows_current() {
330 let mut sink = MockSink;
331 let mut debug = MockDebug::ok();
332 let mut messages = MockMessages;
333 let session = MockSession;
334 let mut agent = crate::NullAgent;
335 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
336 let out = DumpFormatCommand.handle(&mut ctx, "").await.unwrap();
337 let CommandOutput::Message(msg) = out else {
338 panic!("expected Message")
339 };
340 assert!(msg.contains("raw"));
341 }
342
343 #[tokio::test]
344 async fn dump_format_with_arg_switches_format() {
345 let mut sink = MockSink;
346 let mut debug = MockDebug::ok();
347 let mut messages = MockMessages;
348 let session = MockSession;
349 let mut agent = crate::NullAgent;
350 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
351 let out = DumpFormatCommand.handle(&mut ctx, "json").await.unwrap();
352 let CommandOutput::Message(msg) = out else {
353 panic!("expected Message")
354 };
355 assert!(msg.contains("json"));
356 }
357
358 #[test]
359 fn registry_finds_all_debug_commands() {
360 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
361 reg.register(LogCommand);
362 reg.register(DebugDumpCommand);
363 reg.register(DumpFormatCommand);
364
365 assert!(reg.find_handler("/log").is_some());
366 assert!(reg.find_handler("/debug-dump").is_some());
367 assert!(reg.find_handler("/dump-format").is_some());
368 }
369}