use super::state::ToolExecutionState;
use super::ToolCallImpl;
use agent_contracts::runtime::RuntimeView;
use agent_contracts::tool::{ToolExecutor, ToolSpecView};
use agent_types::common::ids::{ToolId, ToolName};
use agent_types::tool::spec_types::{EffectProfile, InputSchemaRef, OutputContract};
use agent_types::tool::{
FinalToolCall, PreHookResult, ToolExecutionError, ToolExecutionResult, ToolExecutorOutput,
};
use async_trait::async_trait;
use std::sync::Arc;
struct TestToolSpec {
id: ToolId,
name: ToolName,
description: String,
input_schema: InputSchemaRef,
}
impl ToolSpecView for TestToolSpec {
fn id(&self) -> &ToolId {
&self.id
}
fn name(&self) -> &ToolName {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn input_schema(&self) -> &InputSchemaRef {
&self.input_schema
}
fn output_contract(&self) -> &OutputContract {
static DEFAULT: OutputContract = OutputContract {
description: String::new(),
};
&DEFAULT
}
fn effect_profile(&self) -> &EffectProfile {
static DEFAULT: EffectProfile = EffectProfile {
reads_filesystem: false,
writes_filesystem: false,
network_access: false,
side_effects: false,
};
&DEFAULT
}
}
struct TestToolExecutor {
spec: Arc<TestToolSpec>,
}
#[async_trait]
impl ToolExecutor for TestToolExecutor {
fn spec(&self) -> &dyn ToolSpecView {
self.spec.as_ref()
}
async fn invoke(
&self,
_call: &FinalToolCall,
_runtime: &dyn RuntimeView,
) -> Result<ToolExecutorOutput, ToolExecutionError> {
panic!("unused in call_impl unit tests")
}
}
fn test_tool_call_impl() -> ToolCallImpl {
let spec = Arc::new(TestToolSpec {
id: ToolId("test_tool".to_string()),
name: ToolName("test_tool".to_string()),
description: "test tool".to_string(),
input_schema: InputSchemaRef {
schema: serde_json::json!({"type": "object"}),
},
});
let executor: Arc<dyn ToolExecutor> = Arc::new(TestToolExecutor { spec: spec.clone() });
let spec: Arc<dyn ToolSpecView> = spec;
ToolCallImpl::new(
FinalToolCall {
call_id: "call-1".to_string(),
tool_name: "test_tool".to_string(),
input: serde_json::json!({"value": 1}),
},
spec,
executor,
)
}
fn test_execution_state() -> ToolExecutionState {
ToolExecutionState {
final_call: FinalToolCall {
call_id: "call-1".to_string(),
tool_name: "test_tool".to_string(),
input: serde_json::json!({"value": 1}),
},
trace_span: None,
lifecycle_record: None,
pre_hook_results: Vec::new(),
post_hook_results: Vec::new(),
error_hook_results: Vec::new(),
raw_outcome: None,
execution_error: None,
}
}
#[test]
fn build_denied_result_uses_pre_hook_reason_when_error_missing() {
let call_impl = test_tool_call_impl();
let mut state = test_execution_state();
state.pre_hook_results = vec![PreHookResult::Deny {
reason: "blocked by policy".to_string(),
}];
let result = call_impl.build_denied_result(&state);
match result {
ToolExecutionResult::Denied { error, .. } => {
assert!(matches!(
error,
Some(ToolExecutionError::PermissionDenied { message }) if message == "blocked by policy"
));
}
other => panic!("expected denied result, got {:?}", other),
}
}
#[test]
fn build_denied_result_preserves_existing_execution_error() {
let call_impl = test_tool_call_impl();
let mut state = test_execution_state();
state.pre_hook_results = vec![PreHookResult::Deny {
reason: "blocked by policy".to_string(),
}];
state.execution_error = Some(ToolExecutionError::ExecutionFailed {
message: "existing error".to_string(),
});
let result = call_impl.build_denied_result(&state);
match result {
ToolExecutionResult::Denied { error, .. } => {
assert!(matches!(
error,
Some(ToolExecutionError::ExecutionFailed { message }) if message == "existing error"
));
}
other => panic!("expected denied result, got {:?}", other),
}
}