use super::advance::{
AGENT, RecLauncher, model_entry, terminal_tail, worker_config, workspace_with_tail,
};
use super::fixtures::*;
use crate::prompt::dispatch::advance::{AdvanceOutcome, run};
use crate::prompt::inbox;
use crate::prompt::resolve::WorkerConfig;
fn compactor_config() -> WorkerConfig {
WorkerConfig {
role: "compactor".into(),
..worker_config()
}
}
#[test]
fn a_pending_worker_result_is_interpreted_then_the_branch_steps() {
use crate::prompt::child_dispatch::{ChildDispatchRequest, run as dispatch_child};
use crate::prompt::inbox::deposit_result;
use crate::template::{GitRunner, RealGit};
use crate::workspace::{agent_worktree, fixture};
let (_h, ws) = fixture::workspace();
let parent = AGENT;
let parent_wt = fixture::spawn_root(&ws, parent);
let git = RealGit::new();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let rec = RecLauncher::default();
let req = ChildDispatchRequest {
repo: &ws,
parent_branch: parent,
parent_worktree: &parent_wt,
role: "worker",
goal: "do it",
fork_point: None,
};
let child = dispatch_child(&req, &git, &clock, &id, &rec).unwrap();
let child_wt = agent_worktree(&ws, &child);
std::fs::write(child_wt.join("out.txt"), "result\n").unwrap();
git.run(&child_wt, &["add", "-A"]).unwrap();
git.run(&child_wt, &["commit", "-m", "work"]).unwrap();
let tip = git.run_capture(&child_wt, &["rev-parse", "HEAD"]).unwrap();
deposit_result(
&ws,
parent,
&child,
inbox::Epitaph::FinalResponse,
tip.trim(),
Some("done"),
&clock,
)
.unwrap();
let adapter = StubAdapter::scripted([StubAdapter::reply_ok(&happy_response_bytes())]);
let (sleeper, tools, stub_git) = (
StubSleeper::default(),
StubToolExecutor::ok(),
StubGit::ok(),
);
let mut deps = valid_deps(&adapter, &sleeper, &stub_git, &clock, &id, &tools, &ws);
deps.git = &git;
deps.launcher = &rec;
let out = run(&ws, parent, None, &deps, &mut || Ok(worker_config())).unwrap();
assert!(matches!(out, AdvanceOutcome::Terminal));
assert_eq!(
std::fs::read_to_string(parent_wt.join("out.txt")).unwrap(),
"result\n"
);
assert!(parent_wt.join(format!("messages/001-{child}.md")).exists());
assert!(
ws.join(format!("steps/{parent}/001/response.json"))
.exists()
);
}
#[test]
fn a_compaction_merge_lands_the_product_and_the_next_step_assembles_clean() {
use crate::prompt::child_dispatch::{ChildDispatchRequest, run as dispatch_child};
use crate::prompt::inbox::deposit_result;
use crate::template::{GitRunner, RealGit};
use crate::workspace::{agent_worktree, fixture};
use brazen::Content;
let (_h, ws) = fixture::workspace();
let parent = AGENT;
let parent_wt = fixture::spawn_root(&ws, parent);
let git = RealGit::new();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let rec = RecLauncher::default();
std::fs::create_dir_all(parent_wt.join("messages")).unwrap();
std::fs::write(parent_wt.join("messages/001-user.md"), "old\n").unwrap();
git.run(&parent_wt, &["add", "-A"]).unwrap();
git.run(&parent_wt, &["commit", "-m", "checkpoint"])
.unwrap();
let req = ChildDispatchRequest {
repo: &ws,
parent_branch: parent,
parent_worktree: &parent_wt,
role: "compactor",
goal: "compact",
fork_point: None,
};
let child = dispatch_child(&req, &git, &clock, &id, &rec).unwrap();
let cwt = agent_worktree(&ws, &child);
std::fs::create_dir_all(cwt.join("summary")).unwrap();
std::fs::write(cwt.join("summary/001.md"), "digest\n").unwrap();
let dialog = [
("002-goal.md", "compact the branch".to_string()),
(
"003-claude-haiku-5.json",
model_entry(&[Content::Text("looking".into())]),
),
(
"004-tool.json",
serde_json::to_string(&[Content::ToolResult {
tool_use_id: "toolu_ghost".into(),
content: vec![Content::Text("no such path".into())],
is_error: true,
}])
.unwrap(),
),
];
for (name, body) in &dialog {
std::fs::write(cwt.join("messages").join(name), body).unwrap();
}
git.run(&cwt, &["rm", "-q", "--", "messages/001-user.md"])
.unwrap();
git.run(&cwt, &["add", "-A"]).unwrap();
git.run(&cwt, &["commit", "-m", "compaction"]).unwrap();
let tip = git.run_capture(&cwt, &["rev-parse", "HEAD"]).unwrap();
deposit_result(
&ws,
parent,
&child,
inbox::Epitaph::FinalResponse,
tip.trim(),
Some("compacted"),
&clock,
)
.unwrap();
inbox::deposit(&ws, parent, "user", "carry on", &clock).unwrap();
let adapter = StubAdapter::scripted([StubAdapter::reply_ok(&happy_response_bytes())]);
let (sleeper, tools, stub_git) = (
StubSleeper::default(),
StubToolExecutor::ok(),
StubGit::ok(),
);
let mut deps = valid_deps(&adapter, &sleeper, &stub_git, &clock, &id, &tools, &ws);
deps.git = &git;
deps.launcher = &rec;
let out = run(&ws, parent, None, &deps, &mut || Ok(worker_config())).unwrap();
assert!(matches!(out, AdvanceOutcome::Terminal), "the step ran");
assert_eq!(
std::fs::read_to_string(parent_wt.join("summary/001.md")).unwrap(),
"digest\n"
);
assert!(!parent_wt.join("messages/001-user.md").exists());
for (name, _) in &dialog {
assert!(
!parent_wt.join("messages").join(name).exists(),
"compactor entry {name} crossed the merge"
);
}
assert!(
!std::fs::read_to_string(parent_wt.join("goal.md"))
.unwrap()
.contains("compact the branch")
);
assert!(
git.run_capture(
&parent_wt,
&[
"cat-file",
"-e",
&format!("agents/{child}:messages/004-tool.json")
]
)
.is_ok()
);
let req: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(ws.join(format!("steps/{parent}/001/request.json"))).unwrap(),
)
.unwrap();
assert!(
!req["messages"].to_string().contains("toolu_ghost"),
"{}",
req["messages"]
);
}
#[test]
fn a_compactor_hop_injects_the_builtin_toolset_into_the_request() {
let (ws, _wt) = workspace_with_tail(&terminal_tail());
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) = (StubSleeper::default(), StubGit::ok());
let tools = 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 req: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(ws.path().join(format!("steps/{AGENT}/001/request.json")))
.unwrap(),
)
.unwrap();
let names: Vec<&str> = req["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
assert!(names.contains(&"write_summary"), "{names:?}");
assert!(names.contains(&"mark_for_deletion"), "{names:?}");
}