use super::{NoAdapter, NoLauncher, NoSleeper, Recorder, Resolution, branch_with_step};
use crate::prompt::clock::SystemClock;
use crate::prompt::compactor::COMPACTOR_ROLE;
use crate::prompt::dispatch::tool_step::{refusal, run_tool_calls};
use crate::template::RealGit;
use brazen::Content;
use serde_json::json;
use std::sync::atomic::AtomicBool;
use tempfile::TempDir;
#[test]
fn an_ungranted_tool_is_declined_in_band_and_the_granted_one_still_runs() {
let agent_id = "agent-5a1f";
let ws = TempDir::new().unwrap();
let git = RealGit::new();
let (worktree, step_dir_rel) = branch_with_step(&ws, agent_id, &git);
let recorder = Recorder(std::cell::RefCell::new(Vec::new()));
let stop = AtomicBool::new(false);
let clock = SystemClock;
let id_gen = crate::prompt::NanoIdGen;
let cfg = TempDir::new().unwrap();
let deps = crate::prompt::Deps {
adapter: &NoAdapter,
sleeper: &NoSleeper,
git: &git,
clock: &clock,
id_gen: &id_gen,
tool_executor: &recorder,
config_root: cfg.path(),
adapter_target: None,
stop: &stop,
launcher: &NoLauncher,
};
let content = vec![
Content::ToolUse {
id: "t_denied".into(),
name: "bash".into(),
input: json!({"command": "rm -rf ."}),
signature: None,
},
Content::ToolUse {
id: "t_granted".into(),
name: "slack_read".into(),
input: json!({"channel": "#ops"}),
signature: None,
},
];
let grant = ["slack_read".to_string(), "message".to_string()];
let resolution = Resolution::new();
let window = run_tool_calls(
ws.path(),
&worktree,
agent_id,
&resolution.of("sensor", &grant),
&step_dir_rel,
&content,
&deps,
)
.unwrap();
assert!(matches!(
window,
crate::prompt::dispatch::tool_step::ToolWindow::Completed
));
assert_eq!(*recorder.0.borrow(), vec![("slack_read".to_string(), None)]);
let entry = std::fs::read_to_string(worktree.join("messages/002-tool.json")).unwrap();
let blocks: Vec<Content> = serde_json::from_str(&entry).unwrap();
let Content::ToolResult {
tool_use_id,
content,
is_error,
} = &blocks[0]
else {
panic!("expected a tool_result, got {:?}", blocks[0]);
};
assert_eq!(tool_use_id, "t_denied");
assert!(is_error, "the decline is an error result");
let Content::Text(text) = &content[0] else {
panic!("the decline is text");
};
assert!(text.contains("\"bash\""), "{text}");
assert!(text.contains("not callable by a sensor"), "{text}");
assert!(text.contains("slack_read, message"), "{text}");
assert!(worktree.join("messages/003-tool.json").exists());
}
#[test]
fn the_decline_names_an_empty_toolset_when_the_role_grants_none() {
let declined = refusal("watcher", &[], "bash").expect("nothing is callable");
assert!(
declined.contains("The watcher toolset is empty"),
"{declined}"
);
}
#[test]
fn a_compactor_calls_its_injected_pair_and_nothing_else() {
assert_eq!(refusal(COMPACTOR_ROLE, &[], "write_summary"), None);
assert_eq!(refusal(COMPACTOR_ROLE, &[], "mark_for_deletion"), None);
let declined = refusal(COMPACTOR_ROLE, &[], "bash").expect("an inherited tool is declined");
assert!(
declined.contains("not callable by a compactor"),
"{declined}"
);
assert!(
declined.contains("write_summary, mark_for_deletion"),
"{declined}"
);
}