zeph_commands/handlers/
plan.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct PlanCommand;
18
19impl CommandHandler<CommandContext<'_>> for PlanCommand {
20 fn name(&self) -> &'static str {
21 "/plan"
22 }
23
24 fn description(&self) -> &'static str {
25 "Create or manage execution plans"
26 }
27
28 fn args_hint(&self) -> &'static str {
29 "[goal|confirm|cancel|status|list|resume|retry]"
30 }
31
32 fn category(&self) -> SlashCategory {
33 SlashCategory::Planning
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.plan.handle");
47 Box::pin(
48 async move {
49 let input = if args.is_empty() {
51 "/plan".to_owned()
52 } else {
53 format!("/plan {args}")
54 };
55 let result = ctx.agent.handle_plan(&input).await?;
56 Ok(CommandOutput::message_or_silent(result))
57 }
58 .instrument(span),
59 )
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::CommandRegistry;
67 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
68 use crate::sink::NullSink;
69 use std::assert_matches;
70
71 #[test]
72 fn plan_name_and_description() {
73 assert_eq!(PlanCommand.name(), "/plan");
74 assert!(!PlanCommand.description().is_empty());
75 }
76
77 #[tokio::test]
78 async fn plan_no_args_returns_silent_when_agent_returns_empty() {
79 let mut sink = NullSink;
81 let mut debug = MockDebug;
82 let mut messages = MockMessages;
83 let session = MockSession;
84 let mut agent = crate::NullAgent;
85 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
86 let out = PlanCommand.handle(&mut ctx, "").await.unwrap();
87 assert_matches!(out, CommandOutput::Silent);
88 }
89
90 #[tokio::test]
91 async fn plan_with_args_returns_silent_when_agent_returns_empty() {
92 let mut sink = NullSink;
93 let mut debug = MockDebug;
94 let mut messages = MockMessages;
95 let session = MockSession;
96 let mut agent = crate::NullAgent;
97 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
98 let out = PlanCommand.handle(&mut ctx, "status").await.unwrap();
99 assert_matches!(out, CommandOutput::Silent);
100 }
101
102 #[tokio::test]
103 async fn plan_dispatch_allowed_when_trusted() {
104 let mut sink = NullSink;
105 let mut debug = MockDebug;
106 let mut messages = MockMessages;
107 let session = MockSession;
108 let mut agent = crate::NullAgent;
109 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
110
111 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
112 reg.register(PlanCommand);
113
114 let result = reg.dispatch(&mut ctx, "/plan status", true).await;
115 assert!(result.unwrap().is_ok());
116 }
117
118 #[tokio::test]
119 async fn plan_dispatch_rejected_when_untrusted() {
120 let mut sink = NullSink;
121 let mut debug = MockDebug;
122 let mut messages = MockMessages;
123 let session = MockSession;
124 let mut agent = crate::NullAgent;
125 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
126
127 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
128 reg.register(PlanCommand);
129
130 let result = reg.dispatch(&mut ctx, "/plan status", false).await;
131 let err = result.unwrap().unwrap_err();
132 assert!(err.0.contains("trusted"));
133 }
134}