Skip to main content

zeph_commands/handlers/
policy.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Policy command handler: `/policy`.
5
6use std::future::Future;
7use std::pin::Pin;
8
9use crate::context::CommandContext;
10use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
11
12/// Inspect policy status or dry-run evaluation.
13///
14/// Subcommands: `status` (default), `check <tool> [args_json]`.
15pub struct PolicyCommand;
16
17impl CommandHandler<CommandContext<'_>> for PolicyCommand {
18    fn name(&self) -> &'static str {
19        "/policy"
20    }
21
22    fn description(&self) -> &'static str {
23        "Inspect policy status or dry-run evaluation"
24    }
25
26    fn args_hint(&self) -> &'static str {
27        "[status|check <tool> [args_json]]"
28    }
29
30    fn category(&self) -> SlashCategory {
31        SlashCategory::Advanced
32    }
33
34    fn feature_gate(&self) -> Option<&'static str> {
35        Some("policy-enforcer")
36    }
37
38    fn requires_auth(&self) -> bool {
39        true
40    }
41
42    fn handle<'a>(
43        &'a self,
44        ctx: &'a mut CommandContext<'_>,
45        args: &'a str,
46    ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
47        use tracing::Instrument as _;
48        let span = tracing::info_span!("commands.policy.handle");
49        Box::pin(
50            async move {
51                let result = ctx.agent.handle_policy(args).await?;
52                Ok(CommandOutput::message_or_silent(result))
53            }
54            .instrument(span),
55        )
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
63    use crate::sink::NullSink;
64    use std::assert_matches;
65
66    #[test]
67    fn policy_name_and_description() {
68        assert_eq!(PolicyCommand.name(), "/policy");
69        assert!(!PolicyCommand.description().is_empty());
70    }
71
72    #[tokio::test]
73    async fn policy_returns_silent_when_agent_returns_empty() {
74        // NullAgent returns empty string, so result is Silent.
75        let mut sink = NullSink;
76        let mut debug = MockDebug;
77        let mut messages = MockMessages;
78        let session = MockSession;
79        let mut agent = crate::NullAgent;
80        let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
81        let out = PolicyCommand.handle(&mut ctx, "status").await.unwrap();
82        assert_matches!(out, CommandOutput::Silent);
83    }
84}