use std::sync::Arc;
use async_trait::async_trait;
use traitclaw_core::prelude::*;
use traitclaw_core::types::completion::{
CompletionRequest, CompletionResponse, ResponseContent, Usage,
};
use traitclaw_core::types::tool_call::ToolCall;
use traitclaw_test_utils::provider::MockProvider;
use traitclaw_test_utils::tools::{DangerousTool, EchoTool};
struct SecurityHook;
#[async_trait]
impl traitclaw_core::traits::hook::AgentHook for SecurityHook {
async fn before_tool_execute(
&self,
name: &str,
_args: &serde_json::Value,
) -> traitclaw_core::traits::hook::HookAction {
if name.contains("dangerous") {
traitclaw_core::traits::hook::HookAction::Block(
"Blocked by security policy: tool name contains 'dangerous'".into(),
)
} else {
traitclaw_core::traits::hook::HookAction::Continue
}
}
}
#[tokio::test]
async fn test_hook_blocks_tool_execution() {
let responses = vec![
CompletionResponse {
content: ResponseContent::ToolCalls(vec![ToolCall {
id: "call_danger".into(),
name: "dangerous_operation".into(),
arguments: serde_json::json!({"payload": "evil"}),
}]),
usage: Usage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
},
CompletionResponse {
content: ResponseContent::Text(
"I understand, the dangerous operation was blocked.".into(),
),
usage: Usage {
prompt_tokens: 30,
completion_tokens: 10,
total_tokens: 40,
},
},
];
let agent = Agent::builder()
.model(MockProvider::sequence(responses))
.system("You are a test bot")
.tool(DangerousTool)
.hook(SecurityHook)
.build()
.unwrap();
let output = agent.run("Run the dangerous operation").await.unwrap();
assert_eq!(
output.text(),
"I understand, the dangerous operation was blocked."
);
}
#[tokio::test]
async fn test_hook_allows_safe_tool_execution() {
let responses = vec![
CompletionResponse {
content: ResponseContent::ToolCalls(vec![ToolCall {
id: "call_safe".into(),
name: "echo".into(),
arguments: serde_json::json!({"text": "safe"}),
}]),
usage: Usage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
},
CompletionResponse {
content: ResponseContent::Text("Echo worked fine".into()),
usage: Usage {
prompt_tokens: 20,
completion_tokens: 5,
total_tokens: 25,
},
},
];
let agent = Agent::builder()
.model(MockProvider::sequence(responses))
.system("You are a test bot")
.tool(EchoTool)
.hook(SecurityHook)
.build()
.unwrap();
let output = agent.run("Use echo").await.unwrap();
assert_eq!(output.text(), "Echo worked fine");
}
struct RecordingHook {
events: std::sync::Mutex<Vec<String>>,
}
impl RecordingHook {
fn new() -> Self {
Self {
events: std::sync::Mutex::new(Vec::new()),
}
}
fn events(&self) -> Vec<String> {
self.events.lock().unwrap().clone()
}
}
#[async_trait]
impl traitclaw_core::traits::hook::AgentHook for RecordingHook {
async fn on_agent_start(&self, _input: &str) {
self.events.lock().unwrap().push("agent_start".into());
}
async fn on_agent_end(
&self,
_output: &traitclaw_core::agent::AgentOutput,
_duration: std::time::Duration,
) {
self.events.lock().unwrap().push("agent_end".into());
}
async fn on_provider_start(&self, _request: &CompletionRequest) {
self.events.lock().unwrap().push("provider_start".into());
}
async fn on_provider_end(
&self,
_response: &CompletionResponse,
_duration: std::time::Duration,
) {
self.events.lock().unwrap().push("provider_end".into());
}
}
#[tokio::test]
async fn test_hook_lifecycle_order() {
let hook = Arc::new(RecordingHook::new());
let agent = Agent::builder()
.model(MockProvider::text("Hello"))
.system("test")
.hook(Arc::clone(&hook))
.build()
.unwrap();
agent.run("Hi").await.unwrap();
let events = hook.events();
assert_eq!(
events,
vec!["agent_start", "provider_start", "provider_end", "agent_end"]
);
}