Skip to main content

traitclaw_test_utils/
tools.rs

1//! Mock tools for testing tool-calling scenarios.
2//!
3//! - [`EchoTool`] — echoes its text input back as output
4//! - [`FailTool`] — always returns an error on execution
5//!
6//! Both tools are designed for use with [`AgentRuntime`](traitclaw_core::traits::strategy::AgentRuntime)
7//! to test tool-calling flows deterministically.
8//!
9//! # Example
10//!
11//! ```rust
12//! use traitclaw_test_utils::tools::{EchoTool, FailTool};
13//! use traitclaw_core::traits::tool::Tool;
14//!
15//! assert_eq!(EchoTool.name(), "echo");
16//! assert_eq!(FailTool.name(), "fail");
17//! ```
18
19use async_trait::async_trait;
20use serde::{Deserialize, Serialize};
21
22use traitclaw_core::traits::tool::Tool;
23use traitclaw_core::{Error, Result};
24
25// ── EchoTool ────────────────────────────────────────────────────────────
26
27/// Input for [`EchoTool`]: a single text field.
28#[derive(Debug, Deserialize, schemars::JsonSchema)]
29pub struct EchoInput {
30    /// The text to echo back.
31    pub text: String,
32}
33
34/// Output from [`EchoTool`]: the echoed text.
35#[derive(Debug, Serialize)]
36pub struct EchoOutput {
37    /// The echoed text (identical to input).
38    pub echo: String,
39}
40
41/// A simple tool that echoes its text input.
42///
43/// Useful for testing that the agent runtime correctly routes
44/// tool calls and processes tool output.
45///
46/// # Example
47///
48/// ```rust
49/// use traitclaw_test_utils::tools::EchoTool;
50/// use traitclaw_core::traits::tool::Tool;
51///
52/// # tokio_test::block_on(async {
53/// let result = EchoTool.execute(traitclaw_test_utils::tools::EchoInput {
54///     text: "hello".into(),
55/// }).await.unwrap();
56/// assert_eq!(result.echo, "hello");
57/// # });
58/// ```
59pub struct EchoTool;
60
61#[async_trait]
62impl Tool for EchoTool {
63    type Input = EchoInput;
64    type Output = EchoOutput;
65
66    fn name(&self) -> &'static str {
67        "echo"
68    }
69
70    fn description(&self) -> &'static str {
71        "Echoes input text back as output"
72    }
73
74    async fn execute(&self, input: Self::Input) -> Result<Self::Output> {
75        Ok(EchoOutput {
76            echo: input.text.clone(),
77        })
78    }
79}
80
81// ── FailTool ────────────────────────────────────────────────────────────
82
83/// Input for [`FailTool`]: an optional message.
84#[derive(Debug, Deserialize, schemars::JsonSchema)]
85pub struct FailInput {
86    /// Optional custom failure message (not used — tool always fails).
87    #[allow(dead_code)]
88    pub message: Option<String>,
89}
90
91/// A tool that always fails with an error.
92///
93/// Useful for testing error handling in agent loops, tool budgets,
94/// and error recovery strategies.
95///
96/// # Example
97///
98/// ```rust
99/// use traitclaw_test_utils::tools::FailTool;
100/// use traitclaw_core::traits::tool::Tool;
101///
102/// # tokio_test::block_on(async {
103/// let result = FailTool.execute(traitclaw_test_utils::tools::FailInput {
104///     message: None,
105/// }).await;
106/// assert!(result.is_err());
107/// # });
108/// ```
109pub struct FailTool;
110
111#[async_trait]
112impl Tool for FailTool {
113    type Input = FailInput;
114    type Output = serde_json::Value;
115
116    fn name(&self) -> &'static str {
117        "fail"
118    }
119
120    fn description(&self) -> &'static str {
121        "Always fails with an error"
122    }
123
124    async fn execute(&self, _input: Self::Input) -> Result<Self::Output> {
125        Err(Error::Runtime("tool failure".into()))
126    }
127}
128
129// ── DangerousTool ──────────────────────────────────────────────────────
130
131/// Input for [`DangerousTool`].
132#[derive(Debug, Deserialize, schemars::JsonSchema)]
133pub struct DangerousInput {
134    /// Payload (unused — tool exists for hook interception tests).
135    #[allow(dead_code)]
136    pub payload: String,
137}
138
139/// Output from [`DangerousTool`].
140#[derive(Debug, Serialize)]
141pub struct DangerousOutput {
142    /// Result text.
143    pub result: String,
144}
145
146/// A mock "dangerous" tool for testing hook-based interception.
147///
148/// Named `"dangerous_operation"` so that hook tests can identify and
149/// intercept it by name before execution.
150///
151/// # Example
152///
153/// ```rust
154/// use traitclaw_test_utils::tools::DangerousTool;
155/// use traitclaw_core::traits::tool::Tool;
156///
157/// assert_eq!(DangerousTool.name(), "dangerous_operation");
158/// ```
159pub struct DangerousTool;
160
161#[async_trait]
162impl Tool for DangerousTool {
163    type Input = DangerousInput;
164    type Output = DangerousOutput;
165
166    fn name(&self) -> &'static str {
167        "dangerous_operation"
168    }
169
170    fn description(&self) -> &'static str {
171        "A dangerous tool for hook interception tests"
172    }
173
174    async fn execute(&self, _input: Self::Input) -> Result<Self::Output> {
175        Ok(DangerousOutput {
176            result: "SHOULD NOT RUN".into(),
177        })
178    }
179}
180
181// ── DenyGuard ──────────────────────────────────────────────────────────
182
183/// A guard that unconditionally denies all actions.
184///
185/// Useful for testing guard interception and abort flows.
186///
187/// # Example
188///
189/// ```rust
190/// use traitclaw_test_utils::tools::DenyGuard;
191/// use traitclaw_core::traits::guard::Guard;
192///
193/// let guard = DenyGuard;
194/// assert_eq!(guard.name(), "deny-all");
195/// ```
196pub struct DenyGuard;
197
198impl traitclaw_core::traits::guard::Guard for DenyGuard {
199    fn name(&self) -> &'static str {
200        "deny-all"
201    }
202
203    fn check(
204        &self,
205        _action: &traitclaw_core::types::action::Action,
206    ) -> traitclaw_core::traits::guard::GuardResult {
207        traitclaw_core::traits::guard::GuardResult::Deny {
208            reason: "blocked by test guard".into(),
209            severity: traitclaw_core::traits::guard::GuardSeverity::High,
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[tokio::test]
219    async fn test_echo_tool_returns_input() {
220        let result = EchoTool
221            .execute(EchoInput {
222                text: "hello world".into(),
223            })
224            .await
225            .unwrap();
226        assert_eq!(result.echo, "hello world");
227    }
228
229    #[tokio::test]
230    async fn test_echo_tool_handles_empty_string() {
231        let result = EchoTool
232            .execute(EchoInput {
233                text: String::new(),
234            })
235            .await
236            .unwrap();
237        assert_eq!(result.echo, "");
238    }
239
240    #[tokio::test]
241    async fn test_fail_tool_returns_error() {
242        let result = FailTool.execute(FailInput { message: None }).await;
243        assert!(result.is_err());
244        let err_str = result.unwrap_err().to_string();
245        assert!(err_str.contains("tool failure"), "got: {err_str}");
246    }
247
248    #[test]
249    fn test_echo_tool_name() {
250        assert_eq!(EchoTool.name(), "echo");
251        assert_eq!(EchoTool.description(), "Echoes input text back as output");
252    }
253
254    #[test]
255    fn test_fail_tool_name() {
256        assert_eq!(FailTool.name(), "fail");
257        assert_eq!(FailTool.description(), "Always fails with an error");
258    }
259
260    #[test]
261    fn test_dangerous_tool_name() {
262        assert_eq!(DangerousTool.name(), "dangerous_operation");
263    }
264
265    #[test]
266    fn test_deny_guard_name() {
267        use traitclaw_core::traits::guard::Guard;
268        assert_eq!(DenyGuard.name(), "deny-all");
269    }
270
271    #[test]
272    fn test_tools_are_send_sync() {
273        fn assert_send_sync<T: Send + Sync>() {}
274        assert_send_sync::<EchoTool>();
275        assert_send_sync::<FailTool>();
276        assert_send_sync::<DangerousTool>();
277        assert_send_sync::<DenyGuard>();
278    }
279}