Skip to main content

zeph_commands/handlers/
trajectory.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! `/trajectory` and `/scope` command handlers (spec 050 Phase 1).
5
6use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12/// Inspect or reset the trajectory risk sentinel.
13///
14/// Subcommands: `status` (default), `reset`.
15pub struct TrajectoryCommand;
16
17impl CommandHandler<CommandContext<'_>> for TrajectoryCommand {
18    fn name(&self) -> &'static str {
19        "/trajectory"
20    }
21
22    fn description(&self) -> &'static str {
23        "Show trajectory risk sentinel status or reset it"
24    }
25
26    fn args_hint(&self) -> &'static str {
27        "[status|reset]"
28    }
29
30    fn category(&self) -> SlashCategory {
31        SlashCategory::Advanced
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.trajectory.handle");
45        Box::pin(
46            async move {
47                let result = ctx.agent.handle_trajectory(args);
48                Ok(CommandOutput::Message(result))
49            }
50            .instrument(span),
51        )
52    }
53}
54
55/// List configured capability scopes.
56///
57/// Subcommands: `list [task_type]` (default).
58pub struct ScopeCommand;
59
60impl CommandHandler<CommandContext<'_>> for ScopeCommand {
61    fn name(&self) -> &'static str {
62        "/scope"
63    }
64
65    fn description(&self) -> &'static str {
66        "List configured capability scopes (spec 050)"
67    }
68
69    fn args_hint(&self) -> &'static str {
70        "[list [task_type]]"
71    }
72
73    fn category(&self) -> SlashCategory {
74        SlashCategory::Advanced
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.scope.handle");
88        Box::pin(
89            async move {
90                let result = ctx.agent.handle_scope(args);
91                Ok(CommandOutput::Message(result))
92            }
93            .instrument(span),
94        )
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
102    use crate::sink::NullSink;
103    use std::assert_matches;
104
105    #[test]
106    fn trajectory_name_and_description() {
107        assert_eq!(TrajectoryCommand.name(), "/trajectory");
108        assert!(!TrajectoryCommand.description().is_empty());
109    }
110
111    #[test]
112    fn scope_name_and_description() {
113        assert_eq!(ScopeCommand.name(), "/scope");
114        assert!(!ScopeCommand.description().is_empty());
115    }
116
117    #[tokio::test]
118    async fn trajectory_returns_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 out = TrajectoryCommand.handle(&mut ctx, "status").await.unwrap();
126        assert_matches!(out, CommandOutput::Message(_));
127    }
128
129    #[tokio::test]
130    async fn scope_returns_message() {
131        let mut sink = NullSink;
132        let mut debug = MockDebug;
133        let mut messages = MockMessages;
134        let session = MockSession;
135        let mut agent = crate::NullAgent;
136        let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
137        let out = ScopeCommand.handle(&mut ctx, "list").await.unwrap();
138        assert_matches!(out, CommandOutput::Message(_));
139    }
140}