use super::advance::{AGENT, RecLauncher, model_entry, worker_config, workspace_with_tail};
use super::fixtures::*;
use crate::prompt::dispatch::advance::run;
use crate::prompt::inbox;
use crate::prompt::resolve::WorkerConfig;
use brazen::{Content, FinishReason};
use serde_json::json;
use std::path::Path;
fn compactor_config() -> WorkerConfig {
WorkerConfig {
role: "compactor".into(),
tools: vec![],
..worker_config()
}
}
const BASH_SCHEMA: &str = r#"{"type":"object","properties":{"command":{"type":"string"}}}"#;
fn tail_that_used_bash() -> Vec<(&'static str, String)> {
vec![
("001-user.md", "do a thing".to_string()),
(
"002-claude-sonnet-5.json",
model_entry(&[Content::ToolUse {
id: "toolu_bash".into(),
name: "bash".into(),
input: json!({"command": "echo hello-from-tool"}),
signature: None,
}]),
),
(
"003-tool.json",
model_entry(&[Content::ToolResult {
tool_use_id: "toolu_bash".into(),
content: vec![Content::Text("hello-from-tool\n".into())],
is_error: false,
}]),
),
]
}
fn inherit_bash_schema(worktree: &Path) {
let dir = worktree.join("descriptions/tools");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("bash.json"), BASH_SCHEMA).unwrap();
}
fn declared_names(workspace: &Path) -> Vec<String> {
let req: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(workspace.join(format!("steps/{AGENT}/001/request.json")))
.unwrap(),
)
.unwrap();
req["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap().to_string())
.collect()
}
#[test]
fn a_compactor_declares_the_inherited_transcripts_tools_alongside_its_own() {
let (ws, wt) = workspace_with_tail(&tail_that_used_bash());
inherit_bash_schema(&wt);
let (clock, id) = (FixedClock::default(), FixedIdGen);
inbox::deposit(ws.path(), AGENT, "user", "compact", &clock).unwrap();
let adapter = StubAdapter::scripted([StubAdapter::reply_ok(&happy_response_bytes())]);
let (sleeper, git, tools) = (
StubSleeper::default(),
StubGit::ok(),
StubToolExecutor::ok(),
);
let rec = RecLauncher::default();
let mut deps = valid_deps(&adapter, &sleeper, &git, &clock, &id, &tools, ws.path());
deps.launcher = &rec;
run(
ws.path(),
AGENT,
None,
&deps,
&mut || Ok(compactor_config()),
)
.unwrap();
let names = declared_names(ws.path());
assert!(names.contains(&"write_summary".to_string()), "{names:?}");
assert!(
names.contains(&"mark_for_deletion".to_string()),
"{names:?}"
);
assert!(names.contains(&"bash".to_string()), "{names:?}");
let req: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(ws.path().join(format!("steps/{AGENT}/001/request.json")))
.unwrap(),
)
.unwrap();
assert!(
req["messages"].to_string().contains("toolu_bash"),
"{}",
req["messages"]
);
let bash = req["tools"]
.as_array()
.unwrap()
.iter()
.find(|t| t["name"] == "bash")
.unwrap();
assert_eq!(
bash["input_schema"],
serde_json::from_str::<serde_json::Value>(BASH_SCHEMA).unwrap()
);
}
#[test]
fn a_worker_hop_declares_only_its_own_tools_when_the_history_used_none() {
let (ws, _wt) = workspace_with_tail(&super::advance::terminal_tail());
let (clock, id) = (FixedClock::default(), FixedIdGen);
inbox::deposit(ws.path(), AGENT, "user", "carry on", &clock).unwrap();
let adapter = StubAdapter::scripted([StubAdapter::reply_ok(&happy_response_bytes())]);
let (sleeper, git, tools) = (
StubSleeper::default(),
StubGit::ok(),
StubToolExecutor::ok(),
);
let rec = RecLauncher::default();
let mut deps = valid_deps(&adapter, &sleeper, &git, &clock, &id, &tools, ws.path());
deps.launcher = &rec;
run(ws.path(), AGENT, None, &deps, &mut || Ok(worker_config())).unwrap();
assert!(declared_names(ws.path()).is_empty());
}
#[test]
fn a_compactor_calling_an_inherited_tool_is_declined_not_executed() {
let (ws, wt) = workspace_with_tail(&tail_that_used_bash());
inherit_bash_schema(&wt);
let (clock, id) = (FixedClock::default(), FixedIdGen);
inbox::deposit(ws.path(), AGENT, "user", "compact", &clock).unwrap();
let reaches_for_bash = stream_of(
FinishReason::ToolUse,
&[Block::ToolUse {
id: "toolu_reach",
name: "bash",
input: json!({"command": "rm -rf ."}),
}],
);
let adapter = StubAdapter::scripted([StubAdapter::reply_ok(&reaches_for_bash)]);
let (sleeper, git, tools) = (
StubSleeper::default(),
StubGit::ok(),
StubToolExecutor::ok(),
);
let rec = RecLauncher::default();
let mut deps = valid_deps(&adapter, &sleeper, &git, &clock, &id, &tools, ws.path());
deps.launcher = &rec;
run(
ws.path(),
AGENT,
None,
&deps,
&mut || Ok(compactor_config()),
)
.unwrap();
assert!(
tools.invocations.borrow().is_empty(),
"a compactor's foreign tool must not execute: {:?}",
tools.invocations.borrow()
);
let entry = std::fs::read_to_string(wt.join("messages/006-tool.json")).unwrap();
let blocks: Vec<Content> = serde_json::from_str(&entry).unwrap();
match &blocks[0] {
Content::ToolResult {
tool_use_id,
content,
is_error,
} => {
assert_eq!(tool_use_id, "toolu_reach");
assert!(is_error, "the decline is an error result");
let Content::Text(text) = &content[0] else {
panic!("decline is text");
};
assert!(text.contains("not callable by a compactor"), "{text}");
assert!(text.contains("write_summary"), "{text}");
}
other => panic!("expected a tool_result, got {other:?}"),
}
}