zeph_commands/handlers/
checkpoint.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct UndoCommand;
16
17impl CommandHandler<CommandContext<'_>> for UndoCommand {
18 fn name(&self) -> &'static str {
19 "/undo"
20 }
21
22 fn description(&self) -> &'static str {
23 "Undo the last N file-mutating shell commands (session-scoped)"
24 }
25
26 fn args_hint(&self) -> &'static str {
27 "[N | list]"
28 }
29
30 fn category(&self) -> SlashCategory {
31 SlashCategory::Session
32 }
33
34 fn requires_auth(&self) -> bool {
35 true
36 }
37
38 fn handle<'a>(
39 &'a self,
40 ctx: &'a mut CommandContext<'_>,
41 args: &'a str,
42 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
43 use tracing::Instrument as _;
44 let span = tracing::info_span!("commands.undo.handle");
45 Box::pin(
46 async move {
47 let result = ctx.agent.handle_undo(args).await?;
48 Ok(CommandOutput::Message(result))
49 }
50 .instrument(span),
51 )
52 }
53}
54
55pub struct RedoCommand;
59
60impl CommandHandler<CommandContext<'_>> for RedoCommand {
61 fn name(&self) -> &'static str {
62 "/redo"
63 }
64
65 fn description(&self) -> &'static str {
66 "Re-apply the last undone shell command"
67 }
68
69 fn args_hint(&self) -> &'static str {
70 ""
71 }
72
73 fn category(&self) -> SlashCategory {
74 SlashCategory::Session
75 }
76
77 fn requires_auth(&self) -> bool {
78 true
79 }
80
81 fn handle<'a>(
82 &'a self,
83 ctx: &'a mut CommandContext<'_>,
84 args: &'a str,
85 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
86 use tracing::Instrument as _;
87 let span = tracing::info_span!("commands.redo.handle");
88 Box::pin(
89 async move {
90 let result = ctx.agent.handle_redo(args).await?;
91 Ok(CommandOutput::Message(result))
92 }
93 .instrument(span),
94 )
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::CommandRegistry;
102 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
103 use crate::sink::NullSink;
104
105 #[test]
106 fn undo_name_and_description() {
107 assert_eq!(UndoCommand.name(), "/undo");
108 assert!(!UndoCommand.description().is_empty());
109 }
110
111 #[test]
112 fn redo_name_and_description() {
113 assert_eq!(RedoCommand.name(), "/redo");
114 assert!(!RedoCommand.description().is_empty());
115 }
116
117 #[tokio::test]
118 async fn undo_not_supported_returns_ok_message() {
119 let mut sink = NullSink;
120 let mut debug = MockDebug;
121 let mut messages = MockMessages;
122 let session = MockSession;
123 let mut agent = crate::NullAgent;
124 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
125 let result = UndoCommand.handle(&mut ctx, "").await;
126 assert!(result.is_ok());
127 if let Ok(CommandOutput::Message(msg)) = result {
128 assert!(!msg.is_empty());
129 }
130 }
131
132 #[tokio::test]
133 async fn redo_not_supported_returns_ok_message() {
134 let mut sink = NullSink;
135 let mut debug = MockDebug;
136 let mut messages = MockMessages;
137 let session = MockSession;
138 let mut agent = crate::NullAgent;
139 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
140 let result = RedoCommand.handle(&mut ctx, "").await;
141 assert!(result.is_ok());
142 if let Ok(CommandOutput::Message(msg)) = result {
143 assert!(!msg.is_empty());
144 }
145 }
146
147 #[tokio::test]
148 async fn undo_dispatch_rejected_when_untrusted() {
149 let mut sink = NullSink;
150 let mut debug = MockDebug;
151 let mut messages = MockMessages;
152 let session = MockSession;
153 let mut agent = crate::NullAgent;
154 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
155
156 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
157 reg.register(UndoCommand);
158
159 let result = reg.dispatch(&mut ctx, "/undo", false).await;
160 let err = result.unwrap().unwrap_err();
161 assert!(err.0.contains("trusted"));
162 }
163
164 #[tokio::test]
165 async fn undo_dispatch_allowed_when_trusted() {
166 let mut sink = NullSink;
167 let mut debug = MockDebug;
168 let mut messages = MockMessages;
169 let session = MockSession;
170 let mut agent = crate::NullAgent;
171 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
172
173 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
174 reg.register(UndoCommand);
175
176 let result = reg.dispatch(&mut ctx, "/undo", true).await;
177 assert!(result.unwrap().is_ok());
178 }
179
180 #[tokio::test]
181 async fn redo_dispatch_rejected_when_untrusted() {
182 let mut sink = NullSink;
183 let mut debug = MockDebug;
184 let mut messages = MockMessages;
185 let session = MockSession;
186 let mut agent = crate::NullAgent;
187 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
188
189 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
190 reg.register(RedoCommand);
191
192 let result = reg.dispatch(&mut ctx, "/redo", false).await;
193 let err = result.unwrap().unwrap_err();
194 assert!(err.0.contains("trusted"));
195 }
196
197 #[tokio::test]
198 async fn redo_dispatch_allowed_when_trusted() {
199 let mut sink = NullSink;
200 let mut debug = MockDebug;
201 let mut messages = MockMessages;
202 let session = MockSession;
203 let mut agent = crate::NullAgent;
204 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
205
206 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
207 reg.register(RedoCommand);
208
209 let result = reg.dispatch(&mut ctx, "/redo", true).await;
210 assert!(result.unwrap().is_ok());
211 }
212}