zeph_commands/handlers/
plugins.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct PluginsCommand;
14
15impl CommandHandler<CommandContext<'_>> for PluginsCommand {
16 fn name(&self) -> &'static str {
17 "/plugins"
18 }
19
20 fn description(&self) -> &'static str {
21 "Manage installed plugins (list, install, remove, update)"
22 }
23
24 fn args_hint(&self) -> &'static str {
25 "[list | install <name> | remove <name> | update [name]]"
26 }
27
28 fn category(&self) -> SlashCategory {
29 SlashCategory::Integration
30 }
31
32 fn requires_auth(&self) -> bool {
33 true
34 }
35
36 fn handle<'a>(
37 &'a self,
38 ctx: &'a mut CommandContext<'_>,
39 args: &'a str,
40 ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
41 use tracing::Instrument as _;
42 let span = tracing::info_span!("commands.plugins.handle");
43 Box::pin(
44 async move {
45 let msg = ctx.agent.handle_plugins(args).await?;
46 Ok(CommandOutput::message_or_silent(msg))
47 }
48 .instrument(span),
49 )
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::CommandRegistry;
57 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
58 use crate::sink::NullSink;
59
60 #[test]
61 fn name_matches_slash_plugins() {
62 assert_eq!(PluginsCommand.name(), "/plugins");
63 }
64
65 #[test]
66 fn category_is_integration() {
67 assert_eq!(PluginsCommand.category(), SlashCategory::Integration);
68 }
69
70 #[test]
71 fn description_is_non_empty() {
72 assert!(!PluginsCommand.description().is_empty());
73 }
74
75 #[test]
76 fn args_hint_is_non_empty() {
77 assert!(!PluginsCommand.args_hint().is_empty());
78 }
79
80 #[tokio::test]
81 async fn plugins_dispatch_allowed_when_trusted() {
82 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
89 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
90 reg.register(PluginsCommand);
91
92 let result = reg.dispatch(&mut ctx, "/plugins list", true).await;
93 assert!(result.unwrap().is_ok());
94 }
95
96 #[tokio::test]
97 async fn plugins_dispatch_rejected_when_untrusted() {
98 let mut sink = NullSink;
99 let mut debug = MockDebug;
100 let mut messages = MockMessages;
101 let session = MockSession;
102 let mut agent = crate::NullAgent;
103 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
104
105 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
106 reg.register(PluginsCommand);
107
108 let result = reg
109 .dispatch(&mut ctx, "/plugins add /etc/passwd", false)
110 .await;
111 let err = result.unwrap().unwrap_err();
112 assert!(err.0.contains("trusted"));
113 }
114}