use super::*;
#[test]
fn read_only_classification_follows_tool_kind_annotations() {
let mut registry = std::collections::BTreeMap::new();
for (name, kind) in [
("read", ToolKind::Read),
("lookup", ToolKind::Read),
("search", ToolKind::Search),
("outline", ToolKind::Search),
("web_search", ToolKind::Search),
("web_fetch", ToolKind::Fetch),
("think", ToolKind::Think),
("write", ToolKind::Edit),
("edit", ToolKind::Edit),
("delete", ToolKind::Delete),
("exec", ToolKind::Execute),
("other", ToolKind::Other),
] {
registry.insert(
name.to_string(),
ToolAnnotations {
kind,
..Default::default()
},
);
}
let policy = crate::orchestration::CapabilityPolicy {
tool_annotations: registry,
..Default::default()
};
push_execution_policy(policy);
let is_ro = |name: &str| {
crate::orchestration::current_tool_annotations(name)
.map(|a| a.kind.is_read_only())
.unwrap_or(false)
};
assert!(is_ro("read"));
assert!(is_ro("lookup"));
assert!(is_ro("search"));
assert!(is_ro("outline"));
assert!(is_ro("web_search"));
assert!(is_ro("web_fetch"));
assert!(is_ro("think"));
assert!(!is_ro("write"));
assert!(!is_ro("edit"));
assert!(!is_ro("delete"));
assert!(!is_ro("exec"));
assert!(!is_ro("other"));
assert!(!is_ro("unknown_tool"));
assert!(!is_ro(""));
pop_execution_policy();
}
#[test]
fn stop_after_successful_tools_matches_successful_turn() {
let stop_tools = vec!["edit".to_string(), "scaffold".to_string()];
let tool_results = vec![
json!({"tool_name": "read", "status": "ok"}),
json!({"tool_name": "edit", "status": "ok"}),
];
assert!(should_stop_after_successful_tools(
&tool_results,
&stop_tools
));
}
#[test]
fn stop_after_successful_tools_ignores_failed_or_unlisted_tools() {
let stop_tools = vec!["edit".to_string()];
let failed_results = vec![json!({"tool_name": "edit", "status": "error"})];
assert!(!should_stop_after_successful_tools(
&failed_results,
&stop_tools
));
let unrelated_results = vec![json!({"tool_name": "read", "status": "ok"})];
assert!(!should_stop_after_successful_tools(
&unrelated_results,
&stop_tools
));
}
#[test]
fn has_successful_tools_matches_any_required_tool() {
let required_tools = vec!["edit".to_string(), "create".to_string()];
let tool_results = vec![
json!({"tool_name": "lookup", "status": "ok"}),
json!({"tool_name": "edit", "status": "ok"}),
];
assert!(has_successful_tools(&tool_results, &required_tools));
}
#[test]
fn has_successful_tools_ignores_failed_turns() {
let required_tools = vec!["edit".to_string()];
let tool_results = vec![json!({"tool_name": "edit", "status": "error"})];
assert!(!has_successful_tools(&tool_results, &required_tools));
}
#[tokio::test(flavor = "current_thread")]
async fn require_successful_tools_marks_loop_failed_when_no_write_succeeds() {
let mut opts = base_opts(vec![serde_json::json!({
"role": "user",
"content": "make a deterministic write",
})]);
let mut config = base_agent_config();
config.require_successful_tools = Some(vec!["edit".to_string()]);
let result = run_agent_loop_internal(&mut opts, config).await.unwrap();
assert_eq!(result["status"], "failed");
assert_eq!(result["tools"]["successful"], json!([]));
}
#[test]
fn tool_kind_is_read_only_excludes_other() {
let annotations = ToolAnnotations {
kind: ToolKind::Other,
..Default::default()
};
assert!(!annotations.kind.is_read_only());
for kind in [
ToolKind::Read,
ToolKind::Search,
ToolKind::Think,
ToolKind::Fetch,
] {
assert!(kind.is_read_only(), "{:?} must be read-only", kind);
}
for kind in [
ToolKind::Edit,
ToolKind::Delete,
ToolKind::Move,
ToolKind::Execute,
] {
assert!(
!kind.is_read_only(),
"{:?} must NOT be read-only (has side effect)",
kind
);
}
}