use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use traitclaw_core::traits::tool::Tool;
use traitclaw_core::{Error, Result};
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct EchoInput {
pub text: String,
}
#[derive(Debug, Serialize)]
pub struct EchoOutput {
pub echo: String,
}
pub struct EchoTool;
#[async_trait]
impl Tool for EchoTool {
type Input = EchoInput;
type Output = EchoOutput;
fn name(&self) -> &'static str {
"echo"
}
fn description(&self) -> &'static str {
"Echoes input text back as output"
}
async fn execute(&self, input: Self::Input) -> Result<Self::Output> {
Ok(EchoOutput {
echo: input.text.clone(),
})
}
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct FailInput {
#[allow(dead_code)]
pub message: Option<String>,
}
pub struct FailTool;
#[async_trait]
impl Tool for FailTool {
type Input = FailInput;
type Output = serde_json::Value;
fn name(&self) -> &'static str {
"fail"
}
fn description(&self) -> &'static str {
"Always fails with an error"
}
async fn execute(&self, _input: Self::Input) -> Result<Self::Output> {
Err(Error::Runtime("tool failure".into()))
}
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct DangerousInput {
#[allow(dead_code)]
pub payload: String,
}
#[derive(Debug, Serialize)]
pub struct DangerousOutput {
pub result: String,
}
pub struct DangerousTool;
#[async_trait]
impl Tool for DangerousTool {
type Input = DangerousInput;
type Output = DangerousOutput;
fn name(&self) -> &'static str {
"dangerous_operation"
}
fn description(&self) -> &'static str {
"A dangerous tool for hook interception tests"
}
async fn execute(&self, _input: Self::Input) -> Result<Self::Output> {
Ok(DangerousOutput {
result: "SHOULD NOT RUN".into(),
})
}
}
pub struct DenyGuard;
impl traitclaw_core::traits::guard::Guard for DenyGuard {
fn name(&self) -> &'static str {
"deny-all"
}
fn check(
&self,
_action: &traitclaw_core::types::action::Action,
) -> traitclaw_core::traits::guard::GuardResult {
traitclaw_core::traits::guard::GuardResult::Deny {
reason: "blocked by test guard".into(),
severity: traitclaw_core::traits::guard::GuardSeverity::High,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_echo_tool_returns_input() {
let result = EchoTool
.execute(EchoInput {
text: "hello world".into(),
})
.await
.unwrap();
assert_eq!(result.echo, "hello world");
}
#[tokio::test]
async fn test_echo_tool_handles_empty_string() {
let result = EchoTool
.execute(EchoInput {
text: String::new(),
})
.await
.unwrap();
assert_eq!(result.echo, "");
}
#[tokio::test]
async fn test_fail_tool_returns_error() {
let result = FailTool.execute(FailInput { message: None }).await;
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(err_str.contains("tool failure"), "got: {err_str}");
}
#[test]
fn test_echo_tool_name() {
assert_eq!(EchoTool.name(), "echo");
assert_eq!(EchoTool.description(), "Echoes input text back as output");
}
#[test]
fn test_fail_tool_name() {
assert_eq!(FailTool.name(), "fail");
assert_eq!(FailTool.description(), "Always fails with an error");
}
#[test]
fn test_dangerous_tool_name() {
assert_eq!(DangerousTool.name(), "dangerous_operation");
}
#[test]
fn test_deny_guard_name() {
use traitclaw_core::traits::guard::Guard;
assert_eq!(DenyGuard.name(), "deny-all");
}
#[test]
fn test_tools_are_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<EchoTool>();
assert_send_sync::<FailTool>();
assert_send_sync::<DangerousTool>();
assert_send_sync::<DenyGuard>();
}
}