zeph_commands/handlers/
search.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12pub struct SearchCommand;
18
19impl CommandHandler<CommandContext<'_>> for SearchCommand {
20 fn name(&self) -> &'static str {
21 "/search"
22 }
23
24 fn description(&self) -> &'static str {
25 "Search the web for a natural-language query (requires tools.search.enabled)"
26 }
27
28 fn args_hint(&self) -> &'static str {
29 "<query> [--limit N]"
30 }
31
32 fn category(&self) -> SlashCategory {
33 SlashCategory::Integration
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.search.handle");
47 Box::pin(
48 async move {
49 let result = ctx.agent.handle_web_search(args).await?;
50 Ok(CommandOutput::Message(result))
51 }
52 .instrument(span),
53 )
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use crate::CommandRegistry;
61 use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
62 use crate::sink::NullSink;
63
64 #[test]
65 fn search_name_and_description() {
66 assert_eq!(SearchCommand.name(), "/search");
67 assert!(!SearchCommand.description().is_empty());
68 }
69
70 #[tokio::test]
71 async fn search_not_supported_returns_ok_message() {
72 let mut sink = NullSink;
73 let mut debug = MockDebug;
74 let mut messages = MockMessages;
75 let session = MockSession;
76 let mut agent = crate::NullAgent;
77 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
78 let result = SearchCommand.handle(&mut ctx, "rust async").await;
79 assert!(result.is_ok());
80 if let Ok(CommandOutput::Message(msg)) = result {
81 assert!(!msg.is_empty());
82 }
83 }
84
85 #[tokio::test]
86 async fn search_dispatch_rejected_when_untrusted() {
87 let mut sink = NullSink;
88 let mut debug = MockDebug;
89 let mut messages = MockMessages;
90 let session = MockSession;
91 let mut agent = crate::NullAgent;
92 let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
93
94 let mut reg: CommandRegistry<CommandContext<'_>> = CommandRegistry::new();
95 reg.register(SearchCommand);
96
97 let result = reg.dispatch(&mut ctx, "/search rust async", false).await;
98 let err = result.unwrap().unwrap_err();
99 assert!(err.0.contains("trusted"));
100 }
101
102 #[tokio::test]
103 async fn search_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(SearchCommand);
113
114 let result = reg.dispatch(&mut ctx, "/search rust async", true).await;
115 assert!(result.unwrap().is_ok());
116 }
117}