mod fork;
use super::*;
use serde_json::json;
use std::cell::RefCell;
use std::io;
use std::path::PathBuf;
#[derive(Default)]
struct StubGit {
runs: RefCell<Vec<(PathBuf, Vec<String>)>>,
fail: bool,
}
impl GitRunner for StubGit {
fn run(&self, dest: &Path, args: &[&str]) -> io::Result<()> {
self.runs.borrow_mut().push((
dest.to_path_buf(),
args.iter().map(|s| (*s).to_owned()).collect(),
));
if self.fail {
Err(io::Error::other("stub git fail"))
} else {
Ok(())
}
}
fn run_capture(&self, _dest: &Path, _args: &[&str]) -> io::Result<String> {
unreachable!("the prune never issues capturing git ops")
}
}
fn tree(entries: &[(&str, String)]) -> tempfile::TempDir {
let dir = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(MESSAGES_DIR)).unwrap();
for (name, body) in entries {
std::fs::write(dir.path().join(MESSAGES_DIR).join(name), body).unwrap();
}
dir
}
fn entry(blocks: &[Content]) -> String {
serde_json::to_string(blocks).unwrap()
}
fn tool_use(id: &str) -> Content {
Content::ToolUse {
id: id.into(),
name: "dispatch".into(),
input: json!({"role": "worker", "goal": "go"}),
signature: None,
}
}
fn tool_result(id: &str) -> Content {
Content::ToolResult {
tool_use_id: id.into(),
content: vec![Content::Text("p1-child".into())],
is_error: false,
}
}
#[test]
fn a_tree_with_no_transcript_issues_no_git_op() {
let wt = tempfile::TempDir::new().unwrap();
let git = StubGit::default();
prune_unsettled(wt.path(), &git).unwrap();
assert!(git.runs.borrow().is_empty());
}
#[test]
fn a_settled_tail_issues_no_git_op() {
let wt = tree(&[
("001-user.md", "do a thing".into()),
("002-claude-sonnet-5.json", entry(&[tool_use("toolu_a")])),
("003-tool.json", entry(&[tool_result("toolu_a")])),
]);
let git = StubGit::default();
prune_unsettled(wt.path(), &git).unwrap();
assert!(git.runs.borrow().is_empty());
}
#[test]
fn a_transcript_of_delivered_messages_alone_issues_no_git_op() {
let wt = tree(&[("001-user.md", "hi".into())]);
let git = StubGit::default();
prune_unsettled(wt.path(), &git).unwrap();
assert!(git.runs.borrow().is_empty());
}
#[test]
fn a_final_response_tail_issues_no_git_op() {
let wt = tree(&[
("001-user.md", "hi".into()),
(
"002-claude-sonnet-5.json",
entry(&[Content::Text("final".into())]),
),
]);
let git = StubGit::default();
prune_unsettled(wt.path(), &git).unwrap();
assert!(git.runs.borrow().is_empty());
}
#[test]
fn the_unsettled_step_and_its_partial_results_are_staged_for_removal() {
let wt = tree(&[
("001-user.md", "do a thing".into()),
(
"002-gpt-5.4.json",
entry(&[
Content::Text("dispatching".into()),
tool_use("call_a"),
tool_use("call_b"),
]),
),
("003-tool.json", entry(&[tool_result("call_a")])),
]);
let git = StubGit::default();
prune_unsettled(wt.path(), &git).unwrap();
let runs = git.runs.borrow();
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].0, wt.path());
assert_eq!(
runs[0].1,
vec![
"rm",
"-q",
"--",
"messages/002-gpt-5.4.json",
"messages/003-tool.json",
],
"the delivered message ahead of the step survives; the step does not"
);
}
#[test]
fn the_cut_follows_the_filename_counter_not_the_readdir_order() {
let wt = tree(&[
("009-tool.json", entry(&[tool_result("call_old")])),
("008-claude-sonnet-5.json", entry(&[tool_use("call_old")])),
("010-gpt-5.4.json", entry(&[tool_use("call_new")])),
("README", "not an entry".into()),
]);
let git = StubGit::default();
prune_unsettled(wt.path(), &git).unwrap();
let runs = git.runs.borrow();
assert_eq!(
runs[0].1,
vec!["rm", "-q", "--", "messages/010-gpt-5.4.json"],
"only the trailing unsettled step is cut; the settled pair below it stays"
);
}
#[test]
fn an_unreadable_transcript_dir_surfaces_rather_than_pruning_blind() {
let wt = tempfile::TempDir::new().unwrap();
std::fs::write(wt.path().join(MESSAGES_DIR), b"not a directory").unwrap();
let git = StubGit::default();
let err = prune_unsettled(wt.path(), &git).unwrap_err();
assert!(matches!(err, Error::Io(_)), "got {err:?}");
}
#[test]
fn an_unreadable_entry_surfaces_rather_than_pruning_blind() {
let wt = tree(&[("001-claude-sonnet-5.json", entry(&[tool_use("call_a")]))]);
std::fs::remove_file(wt.path().join("messages/001-claude-sonnet-5.json")).unwrap();
std::fs::create_dir(wt.path().join("messages/001-claude-sonnet-5.json")).unwrap();
let git = StubGit::default();
let err = prune_unsettled(wt.path(), &git).unwrap_err();
assert!(matches!(err, Error::Io(_)), "got {err:?}");
}
#[test]
fn a_failing_git_rm_surfaces_as_a_named_git_error() {
let wt = tree(&[("001-claude-sonnet-5.json", entry(&[tool_use("call_a")]))]);
let git = StubGit {
fail: true,
..StubGit::default()
};
let err = prune_unsettled(wt.path(), &git).unwrap_err();
assert!(
matches!(&err, Error::Git { op, .. } if *op == "rm unsettled tool step"),
"got {err:?}"
);
}