zeph_commands/handlers/
worktree.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct WorktreeCommand;
18
19impl CommandHandler<CommandContext<'_>> for WorktreeCommand {
20 fn name(&self) -> &'static str {
21 "/worktree"
22 }
23
24 fn description(&self) -> &'static str {
25 "List or clean the live session's git worktrees"
26 }
27
28 fn args_hint(&self) -> &'static str {
29 "list | clean [--force]"
30 }
31
32 fn category(&self) -> SlashCategory {
33 SlashCategory::Advanced
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.worktree.handle");
47 Box::pin(
48 async move {
49 let words: Vec<&str> = args.split_whitespace().collect();
50 let result = match words.as_slice() {
51 [] | ["list"] => ctx.agent.list_worktrees().await?,
52 ["clean"] => ctx.agent.clean_worktrees(false).await?,
53 ["clean", "--force"] => ctx.agent.clean_worktrees(true).await?,
54 _ => {
55 return Err(CommandError::new(
56 "Unknown /worktree subcommand. Available: /worktree list, \
57 /worktree clean [--force]",
58 ));
59 }
60 };
61 match result {
62 Some(msg) => Ok(CommandOutput::Message(msg)),
63 None => Ok(CommandOutput::Message(
64 "Worktree subsystem is not enabled for this session.".to_owned(),
65 )),
66 }
67 }
68 .instrument(span),
69 )
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
77 use crate::sink::NullSink;
78
79 #[test]
80 fn worktree_name_and_description() {
81 assert_eq!(WorktreeCommand.name(), "/worktree");
82 assert!(!WorktreeCommand.description().is_empty());
83 }
84
85 #[tokio::test]
86 async fn worktree_none_returns_not_enabled_message() {
87 let mut sink = NullSink;
89 let mut debug = MockDebug;
90 let mut messages = MockMessages;
91 let session = MockSession;
92 let mut agent = crate::NullAgent;
93 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
94 let out = WorktreeCommand.handle(&mut ctx, "").await.unwrap();
95 let CommandOutput::Message(msg) = out else {
96 panic!("expected Message")
97 };
98 assert!(msg.contains("not enabled"));
99 }
100
101 #[tokio::test]
102 async fn worktree_list_subcommand_returns_not_enabled_message() {
103 let mut sink = NullSink;
104 let mut debug = MockDebug;
105 let mut messages = MockMessages;
106 let session = MockSession;
107 let mut agent = crate::NullAgent;
108 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
109 let out = WorktreeCommand.handle(&mut ctx, "list").await.unwrap();
110 let CommandOutput::Message(msg) = out else {
111 panic!("expected Message")
112 };
113 assert!(msg.contains("not enabled"));
114 }
115
116 #[tokio::test]
117 async fn worktree_clean_force_subcommand_returns_not_enabled_message() {
118 let mut sink = NullSink;
119 let mut debug = MockDebug;
120 let mut messages = MockMessages;
121 let session = MockSession;
122 let mut agent = crate::NullAgent;
123 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
124 let out = WorktreeCommand
125 .handle(&mut ctx, "clean --force")
126 .await
127 .unwrap();
128 let CommandOutput::Message(msg) = out else {
129 panic!("expected Message")
130 };
131 assert!(msg.contains("not enabled"));
132 }
133
134 #[tokio::test]
135 async fn worktree_clean_force_tolerates_irregular_whitespace() {
136 let mut sink = NullSink;
139 let mut debug = MockDebug;
140 let mut messages = MockMessages;
141 let session = MockSession;
142 let mut agent = crate::NullAgent;
143 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
144 let out = WorktreeCommand
145 .handle(&mut ctx, " clean --force ")
146 .await
147 .unwrap();
148 let CommandOutput::Message(msg) = out else {
149 panic!("expected Message")
150 };
151 assert!(msg.contains("not enabled"));
152 }
153
154 #[tokio::test]
155 async fn worktree_unknown_subcommand_returns_error() {
156 let mut sink = NullSink;
157 let mut debug = MockDebug;
158 let mut messages = MockMessages;
159 let session = MockSession;
160 let mut agent = crate::NullAgent;
161 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
162 let err = WorktreeCommand.handle(&mut ctx, "bogus").await.unwrap_err();
163 assert!(err.to_string().contains("Unknown"));
164 }
165}