zeph_commands/handlers/
goal.rs1use std::future::Future;
16use std::pin::Pin;
17
18use crate::context::CommandContext;
19use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
20
21pub struct GoalCommand;
27
28impl CommandHandler<CommandContext<'_>> for GoalCommand {
29 fn name(&self) -> &'static str {
30 "/goal"
31 }
32
33 fn description(&self) -> &'static str {
34 "Manage long-horizon goals that persist across conversation turns"
35 }
36
37 fn args_hint(&self) -> &'static str {
38 "create <text> [--budget N] | pause | resume | complete | clear | status | list"
39 }
40
41 fn category(&self) -> SlashCategory {
42 SlashCategory::Session
43 }
44
45 fn requires_auth(&self) -> bool {
46 true
47 }
48
49 fn handle<'a>(
50 &'a self,
51 ctx: &'a mut CommandContext<'_>,
52 args: &'a str,
53 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
54 use tracing::Instrument as _;
55 let span = tracing::info_span!("commands.goal.handle");
56 Box::pin(
57 async move {
58 let result = ctx.agent.handle_goal(args).await?;
59 Ok(CommandOutput::Message(result))
60 }
61 .instrument(span),
62 )
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::CommandRegistry;
70 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
71 use crate::sink::NullSink;
72
73 #[test]
74 fn goal_name_and_description() {
75 assert_eq!(GoalCommand.name(), "/goal");
76 assert!(!GoalCommand.description().is_empty());
77 }
78
79 #[tokio::test]
80 async fn goal_propagates_error_from_agent() {
81 let mut sink = NullSink;
83 let mut debug = MockDebug;
84 let mut messages = MockMessages;
85 let session = MockSession;
86 let mut agent = crate::NullAgent;
87 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
88 let result = GoalCommand.handle(&mut ctx, "status").await;
89 assert!(result.is_err());
90 }
91
92 #[tokio::test]
93 async fn goal_with_empty_args_propagates_error() {
94 let mut sink = NullSink;
95 let mut debug = MockDebug;
96 let mut messages = MockMessages;
97 let session = MockSession;
98 let mut agent = crate::NullAgent;
99 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
100 let result = GoalCommand.handle(&mut ctx, "").await;
101 assert!(result.is_err());
102 }
103
104 #[tokio::test]
105 async fn goal_dispatch_allowed_when_trusted() {
106 let mut sink = NullSink;
109 let mut debug = MockDebug;
110 let mut messages = MockMessages;
111 let session = MockSession;
112 let mut agent = crate::NullAgent;
113 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
114
115 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
116 reg.register(GoalCommand);
117
118 let result = reg.dispatch(&mut ctx, "/goal status", true).await;
119 let err = result.unwrap().unwrap_err();
120 assert!(!err.0.contains("trusted"));
121 }
122
123 #[tokio::test]
124 async fn goal_dispatch_rejected_when_untrusted() {
125 let mut sink = NullSink;
126 let mut debug = MockDebug;
127 let mut messages = MockMessages;
128 let session = MockSession;
129 let mut agent = crate::NullAgent;
130 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
131
132 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
133 reg.register(GoalCommand);
134
135 let result = reg.dispatch(&mut ctx, "/goal status", false).await;
136 let err = result.unwrap().unwrap_err();
137 assert!(err.0.contains("trusted"));
138 }
139}