mod common;
use common::write_fast_script;
use std::{
fs,
os::unix::fs::PermissionsExt as _,
path::{Path, PathBuf},
process::Command,
};
use serde_json::Value;
use tempfile::TempDir;
#[test]
fn prune_reclaims_decided_worktrees_and_keeps_every_sealed_record() {
let fixture = Fixture::new();
let clean = fixture.run_task("clean");
let committed = fixture.run_task("committed");
let undecided = fixture.run_task("undecided");
fixture.accept(&clean);
fixture.accept(&committed);
fixture.commit_in_worktree(&committed);
let report = fixture.prune(&[]);
assert_eq!(report["applied"], false);
assert_eq!(report["removed"], 0);
assert_eq!(report["removable"], 2);
assert_eq!(report["kept"], 1);
assert!(
fixture.worktree(&clean).is_dir(),
"report mode removed a worktree"
);
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["removed"], 2);
assert_eq!(applied["removed_keeping_branch"], 1);
assert_eq!(applied["kept"], 1);
assert!(!fixture.worktree(&clean).exists());
assert!(!fixture.worktree(&committed).exists());
assert!(fixture.worktree(&undecided).is_dir());
assert_eq!(
status_of(&applied, &Fixture::slug(&committed)),
"removed_branch_kept",
"a reclaimed checkout whose branch git kept must not report as kept"
);
let branches = fixture.branches();
assert!(
!branches.contains(&format!("brgr/task-{}", Fixture::slug(&clean))),
"merged branch survived: {branches:?}"
);
assert!(
branches.contains(&format!("brgr/task-{}", Fixture::slug(&committed))),
"committed work lost its branch: {branches:?}"
);
assert!(
branches.contains(&format!("brgr/task-{}", Fixture::slug(&undecided))),
"undecided branch was deleted: {branches:?}"
);
assert!(
branches.iter().any(|branch| branch == "user/keep-me"),
"a branch brgr does not own was removed: {branches:?}"
);
for task in [&clean, &committed, &undecided] {
let detail = fixture.json(&["result", task]);
assert_eq!(detail["result"]["outcome"], "candidate", "lost {task}");
assert_eq!(detail["artifacts"].as_array().unwrap().len(), 1);
}
}
#[test]
fn prune_refuses_ignored_files_symlinks_foreign_directories_and_its_own_cwd() {
let fixture = Fixture::new();
let ignored = fixture.run_task("ignored");
let cwd = fixture.run_task("cwd");
fixture.accept(&ignored);
fs::write(fixture.worktree(&ignored).join(".env"), b"SECRET=1\n").unwrap();
let escape = fixture.worktrees_root().join("repo").join("aaaabbbb");
std::os::unix::fs::symlink(&fixture.repository, &escape).unwrap();
fs::create_dir_all(fixture.worktrees_root().join("repo").join("cafe")).unwrap();
let locks = fixture.worktrees_root().join(".locks");
fs::create_dir_all(&locks).unwrap();
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["removed"], 0, "{applied}");
assert!(
fixture.worktree(&ignored).join(".env").is_file(),
"an ignored file git cannot see was destroyed"
);
assert!(
reason_of(&applied, ".env-holder", &Fixture::slug(&ignored)).contains("ignored path"),
"{applied}"
);
assert!(
reason_of(&applied, "symlink", "aaaabbbb").contains("symlink"),
"{applied}"
);
assert!(
reason_of(&applied, "foreign directory", "cafe").contains("not a brgr task slug"),
"{applied}"
);
assert!(
fixture.repository.join(".git").is_dir(),
"the symlink target repository was destroyed"
);
assert!(
locks.is_dir(),
"brgr's admission lock directory was removed"
);
let opted_in = fixture.prune(&["--apply", "--include-ignored"]);
assert_eq!(opted_in["removed"], 1, "{opted_in}");
assert!(!fixture.worktree(&ignored).exists());
fixture.accept(&cwd);
let from_inside = fixture.prune_in(fixture.worktree(&cwd), &["--apply"]);
assert_eq!(from_inside["removed"], 0, "{from_inside}");
assert!(fixture.worktree(&cwd).is_dir());
assert!(
reason_of(&from_inside, "cwd", &Fixture::slug(&cwd)).contains("current working directory"),
"{from_inside}"
);
}
#[test]
fn an_acknowledged_failure_is_reclaimed_and_an_unacknowledged_one_is_kept() {
let fixture = Fixture::new();
let seen = fixture.run_failing_task("seen");
let unseen = fixture.run_failing_task("unseen");
fixture.acknowledge(&seen);
let report = fixture.prune(&[]);
assert_eq!(
status_of(&report, &Fixture::slug(&seen)),
"removable",
"{report}"
);
let reason = reason_of(&report, "unseen", &Fixture::slug(&unseen));
assert!(reason.contains("not acknowledged"), "{reason}");
assert!(
reason.contains(&format!("brgr result {unseen} --ack")),
"{reason}"
);
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["removed"], 1, "{applied}");
assert!(!fixture.worktree(&seen).exists());
assert!(fixture.worktree(&unseen).is_dir());
assert_ne!(
fixture.json(&["result", &seen])["result"]["outcome"],
"candidate"
);
}
#[test]
fn an_acknowledged_failure_is_kept_while_its_worker_still_runs() {
let fixture = Fixture::new();
let task = fixture.run_failing_task("orphaned worker");
fixture.acknowledge(&task);
let mut worker = Command::new("/bin/sleep").arg("60").spawn().unwrap();
let pid_file = fixture.home.join("runs").join(format!("{task}.pid"));
fs::write(&pid_file, format!("{}\n", worker.id())).unwrap();
let kept = fixture.prune(&["--apply"]);
let reason = reason_of(&kept, "running", &Fixture::slug(&task));
assert!(reason.contains("still running"), "{reason}");
assert!(fixture.worktree(&task).is_dir());
worker.kill().unwrap();
worker.wait().unwrap();
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["removed"], 1, "{applied}");
assert!(!fixture.worktree(&task).exists());
}
#[test]
fn an_orphan_branch_of_an_acknowledged_failure_is_reclaimed() {
let fixture = Fixture::new();
let task = fixture.run_failing_task("gone failure");
fixture.acknowledge(&task);
fs::remove_dir_all(fixture.worktree(&task)).unwrap();
let branch = format!("brgr/task-{}", Fixture::slug(&task));
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["orphan_branches_removed"], 1, "{applied}");
assert!(!fixture.branches().contains(&branch));
}
#[test]
fn a_locked_worktree_is_kept_in_both_modes_rather_than_promised_then_refused() {
let fixture = Fixture::new();
let locked = fixture.run_task("locked");
fixture.accept(&locked);
fixture.git(&[
"worktree",
"lock",
fixture.worktree(&locked).to_str().unwrap(),
]);
let report = fixture.prune(&[]);
assert_eq!(report["removable"], 0, "{report}");
assert!(
reason_of(&report, "locked", &Fixture::slug(&locked)).contains("locked"),
"{report}"
);
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["removed"], 0, "{applied}");
assert!(fixture.worktree(&locked).is_dir());
}
#[test]
fn a_branch_left_by_a_hand_removed_worktree_is_reclaimed() {
let fixture = Fixture::new();
let gone = fixture.run_task("gone");
let kept = fixture.run_task("kept");
fixture.accept(&gone);
fs::remove_dir_all(fixture.worktree(&gone)).unwrap();
let branch = format!("brgr/task-{}", Fixture::slug(&gone));
let report = fixture.prune(&[]);
assert_eq!(report["orphan_branches_removable"], 1, "{report}");
assert!(
fixture.branches().contains(&branch),
"report mode deleted a branch"
);
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["orphan_branches_removed"], 1, "{applied}");
assert!(
!fixture.branches().contains(&branch),
"orphan branch survived: {:?}",
fixture.branches()
);
assert!(
fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&kept)))
);
assert!(fixture.worktree(&kept).is_dir());
let listing = fixture.git_stdout(&["worktree", "list"]);
assert!(
!listing.contains(&Fixture::slug(&gone)),
"stale registration survived: {listing}"
);
}
#[test]
fn an_unreadable_directory_is_one_kept_row_rather_than_an_aborted_sweep() {
let fixture = Fixture::new();
let settled = fixture.run_task("settled");
fixture.accept(&settled);
let blocked = fixture.worktrees_root().join("unreadable");
fs::create_dir_all(&blocked).unwrap();
fs::set_permissions(&blocked, fs::Permissions::from_mode(0o000)).unwrap();
let applied = fixture.prune(&["--apply"]);
fs::set_permissions(&blocked, fs::Permissions::from_mode(0o755)).unwrap();
assert_eq!(applied["removed"], 1, "{applied}");
assert!(
reason_of(&applied, "unreadable", "unreadable").contains("could not be read"),
"{applied}"
);
}
#[test]
fn a_report_scales_by_repository_rather_than_by_candidate() {
let fixture = Fixture::new();
for index in 0..4 {
let task = fixture.run_task(&format!("task {index}"));
fixture.accept(&task);
}
let calls = fixture.prune_counting_git(&[]);
let listings = calls
.iter()
.filter(|call| call.contains("worktree list"))
.count();
assert_eq!(
listings, 1,
"the repository inventory was read {listings} times for four candidates: {calls:?}"
);
let applied = fixture.prune_counting_git(&["--apply"]);
assert!(
applied
.iter()
.filter(|call| call.contains("worktree list"))
.count()
<= 2,
"{applied:?}"
);
let statuses = calls.iter().filter(|call| call.contains("status")).count();
assert_eq!(statuses, 4, "{calls:?}");
}
fn status_of(receipt: &Value, slug: &str) -> String {
row(receipt, slug)["status"]
.as_str()
.unwrap_or("")
.to_owned()
}
#[test]
fn a_stray_checkout_of_another_repository_is_never_asked_which_repository_it_is() {
let fixture = Fixture::new();
let settled = fixture.run_task("settled");
fixture.accept(&settled);
let other = fixture.temp.path().join("other");
fs::create_dir_all(&other).unwrap();
git(&other, &["init", "-q"]);
git(&other, &["commit", "-q", "--allow-empty", "-m", "init"]);
git(&other, &["branch", "brgr/task-cafed00d"]);
let stray = fixture.worktrees_root().join("repo").join("+backup");
git(
&other,
&["worktree", "add", "-q", "--detach", stray.to_str().unwrap()],
);
let report = fixture.prune(&[]);
assert_eq!(report["removable"], 1, "{report}");
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["removed"], 1, "{applied}");
assert!(!fixture.worktree(&settled).exists());
assert!(
branches_of(&other).contains(&"brgr/task-cafed00d".to_owned()),
"prune deleted a branch in an unrelated repository"
);
assert!(stray.is_dir(), "prune removed a checkout it did not create");
}
#[test]
fn a_worktree_brgr_did_not_create_is_kept_even_under_a_task_name() {
let fixture = Fixture::new();
let settled = fixture.run_task("settled");
fixture.accept(&settled);
let theirs = fixture
.worktrees_root()
.join("elsewhere")
.join(Fixture::slug(&settled));
fixture.git(&[
"worktree",
"add",
"-q",
"--detach",
theirs.to_str().unwrap(),
]);
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["removed"], 1, "{applied}");
assert!(!fixture.worktree(&settled).exists());
assert!(
theirs.is_dir(),
"prune removed a worktree it did not create"
);
assert!(
!applied.to_string().contains("cannot be located"),
"{applied}"
);
}
#[test]
fn branches_are_reclaimed_when_every_worktree_was_removed_by_hand() {
let fixture = Fixture::new();
let first = fixture.run_task("first");
let second = fixture.run_task("second");
fixture.accept(&first);
fixture.accept(&second);
fs::remove_dir_all(fixture.worktree(&first)).unwrap();
fs::remove_dir_all(fixture.worktree(&second)).unwrap();
let report = fixture.prune(&[]);
assert_eq!(report["orphan_branches_removable"], 2, "{report}");
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["orphan_branches_removed"], 2, "{applied}");
assert!(
!fixture
.branches()
.iter()
.any(|branch| branch.starts_with("brgr/task-")),
"{:?}",
fixture.branches()
);
assert_eq!(
fixture
.git_stdout(&["worktree", "list", "--porcelain"])
.matches("worktree ")
.count(),
1,
"stale registrations survived"
);
}
#[test]
fn reclaiming_an_orphan_leaves_the_users_own_missing_worktree_registered() {
let fixture = Fixture::new();
let _anchor = fixture.run_task("anchor");
let volume = fixture.temp.path().join("unmounted").join("scratch");
fixture.git(&[
"worktree",
"add",
"-q",
"--detach",
volume.to_str().unwrap(),
]);
fs::remove_dir_all(fixture.temp.path().join("unmounted")).unwrap();
let gone = fixture.run_task("gone");
fixture.accept(&gone);
fs::remove_dir_all(fixture.worktree(&gone)).unwrap();
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["orphan_branches_removed"], 1, "{applied}");
let listing = fixture.git_stdout(&["worktree", "list", "--porcelain"]);
assert!(
listing.contains("unmounted/scratch"),
"prune dropped a registration it did not create:\n{listing}"
);
assert!(
!listing.contains(&Fixture::slug(&gone)),
"the task's own stale registration survived:\n{listing}"
);
}
#[test]
fn an_orphan_with_unmerged_commits_is_kept_in_report_mode_too() {
let fixture = Fixture::new();
let _anchor = fixture.run_task("anchor");
let committed = fixture.run_task("committed");
fixture.accept(&committed);
fixture.commit_in_worktree(&committed);
fs::remove_dir_all(fixture.worktree(&committed)).unwrap();
let report = fixture.prune(&[]);
assert_eq!(report["orphan_branches_removable"], 0, "{report}");
assert!(orphan_reason(&report).contains("not merged"), "{report}");
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["orphan_branches_removed"], 0, "{applied}");
assert!(
fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&committed)))
);
}
#[test]
fn a_task_branch_from_a_feature_branch_goes_while_that_branch_keeps_its_commits() {
let fixture = Fixture::new();
fixture.commit_on_new_branch("feature");
let task = fixture.run_task("on feature");
fixture.accept(&task);
fixture.git(&["checkout", "-q", "main"]);
let applied = fixture.prune(&["--apply"]);
assert_eq!(
status_of(&applied, &Fixture::slug(&task)),
"removed",
"{applied}"
);
assert!(
!fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&task)))
);
assert!(fixture.branches().contains(&"feature".to_owned()));
}
#[test]
fn commits_held_only_by_other_task_branches_keep_every_branch() {
let fixture = Fixture::new();
fixture.commit_on_new_branch("feature");
let first = fixture.run_task("first");
let second = fixture.run_task("second");
fixture.accept(&first);
fixture.accept(&second);
fixture.git(&["checkout", "-q", "main"]);
fixture.git(&["branch", "-D", "feature"]);
let applied = fixture.prune(&["--apply"]);
for task in [&first, &second] {
let branch = format!("brgr/task-{}", Fixture::slug(task));
assert!(
fixture.branches().contains(&branch),
"{branch} was deleted: {applied}"
);
let reason = reason_of(&applied, "kept branch", &Fixture::slug(task));
assert!(reason.contains("no other branch or remote"), "{reason}");
}
}
#[test]
fn a_remote_tracking_branch_preserves_the_commits_too() {
let fixture = Fixture::new();
let remote = fixture.temp.path().join("remote.git");
git(
fixture.temp.path(),
&["init", "-q", "--bare", remote.to_str().unwrap()],
);
fixture.git(&["remote", "add", "origin", remote.to_str().unwrap()]);
fixture.commit_on_new_branch("feature");
fixture.git(&["push", "-q", "origin", "feature"]);
let task = fixture.run_task("pushed");
fixture.accept(&task);
fixture.git(&["checkout", "-q", "main"]);
fixture.git(&["branch", "-D", "feature"]);
fs::remove_dir_all(fixture.worktree(&task)).unwrap();
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["orphan_branches_removed"], 1, "{applied}");
assert!(
!fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&task)))
);
}
#[test]
fn an_orphan_of_an_undecided_revision_is_kept_and_names_its_owner() {
let fixture = Fixture::new();
let _anchor = fixture.run_task("anchor");
let undecided = fixture.run_task("undecided");
fs::remove_dir_all(fixture.worktree(&undecided)).unwrap();
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["orphan_branches_removed"], 0, "{applied}");
assert!(orphan_reason(&applied).contains("decision"), "{applied}");
assert_eq!(
applied["orphan_branches"][0]["owner_id"], "codex:prune-owner",
"{applied}"
);
assert!(
fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&undecided)))
);
}
#[test]
fn a_task_admitted_before_checkouts_were_recorded_is_located_through_git() {
let fixture = Fixture::new();
let gone = fixture.run_task("gone");
let kept = fixture.run_task("kept");
fixture.accept(&gone);
fixture.forget_checkouts();
fs::remove_dir_all(fixture.worktree(&gone)).unwrap();
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["orphan_branches_removed"], 1, "{applied}");
assert!(fixture.worktree(&kept).is_dir());
}
#[test]
fn a_repository_nobody_can_locate_is_reported_rather_than_counted_as_clean() {
let fixture = Fixture::new();
let gone = fixture.run_task("gone");
fixture.accept(&gone);
fixture.forget_checkouts();
fs::remove_dir_all(fixture.worktree(&gone)).unwrap();
let report = fixture.prune(&[]);
assert_eq!(report["orphan_branches_removable"], 0, "{report}");
let reason = reason_of(&report, "unlocated repository", "repo");
assert!(reason.contains("cannot be located"), "{report}");
}
fn orphan_reason(receipt: &Value) -> String {
receipt["orphan_branches"][0]["reason"]
.as_str()
.unwrap_or_else(|| panic!("no orphan reason in {receipt}"))
.to_owned()
}
fn branches_of(repository: &Path) -> Vec<String> {
let output = Command::new("git")
.arg("-C")
.arg(repository)
.args(["branch", "--format=%(refname:short)"])
.output()
.unwrap();
String::from_utf8_lossy(&output.stdout)
.lines()
.map(|line| line.trim().to_owned())
.collect()
}
fn reason_of(receipt: &Value, label: &str, slug: &str) -> String {
let row = row(receipt, slug);
row["reason"]
.as_str()
.unwrap_or_else(|| panic!("{label} ({slug}) has no reason in {receipt}"))
.to_owned()
}
fn row<'a>(receipt: &'a Value, slug: &str) -> &'a Value {
receipt["worktrees"]
.as_array()
.unwrap()
.iter()
.find(|row| row["task_slug"] == slug)
.unwrap_or_else(|| panic!("no row for {slug} in {receipt}"))
}
#[test]
fn a_decision_removes_a_worktree_with_ignored_files_only_when_told_to() {
let fixture = Fixture::new();
let kept = fixture.run_with("kept", &[]);
let removed = fixture.run_with("removed", &[]);
for task in [&kept, &removed] {
fs::write(fixture.worktree(task).join(".env"), b"CACHE=1\n").unwrap();
}
fixture.json(&["accept", &kept, "--reason", "verified"]);
assert!(
fixture.worktree(&kept).join(".env").is_file(),
"an ignored file was removed without the owner asking"
);
fixture.json(&[
"accept",
&removed,
"--reason",
"verified",
"--include-ignored",
]);
assert!(
!fixture.worktree(&removed).exists(),
"--include-ignored left the worktree"
);
assert!(
!fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&removed)))
);
assert_eq!(
fixture.json(&["result", &removed])["result"]["outcome"],
"candidate"
);
}
#[test]
fn a_leftover_include_ignored_marker_does_not_apply_to_a_later_decision() {
let fixture = Fixture::new();
let task = fixture.run_with("stale", &[]);
fs::write(fixture.worktree(&task).join(".env"), b"CACHE=1\n").unwrap();
fs::write(
fixture
.home
.join("runs")
.join(format!("{task}-r1.include-ignored")),
b"decision-that-was-never-recorded",
)
.unwrap();
fixture.json(&["reject", &task, "--reason", "keep my ignored files"]);
assert!(
fixture.worktree(&task).join(".env").is_file(),
"a decision made without --include-ignored deleted an ignored file"
);
}
#[test]
fn accepting_a_clean_task_reclaims_its_worktree_and_branch_at_once() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &[]);
assert!(fixture.worktree(&task).is_dir());
fixture.accept(&task);
assert!(
!fixture.worktree(&task).exists(),
"worktree survived its decision"
);
assert!(
!fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&task)))
);
let detail = fixture.json(&["result", &task]);
assert_eq!(detail["result"]["outcome"], "candidate");
}
#[test]
fn a_reclaim_already_in_progress_is_left_to_its_owner_without_a_note() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &[]);
let lock = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(
fixture
.home
.join("runs")
.join(format!("{task}-r1.reclaim.lock")),
)
.unwrap();
lock.try_lock().unwrap();
let output = fixture
.command()
.args(["accept", &task, "--reason", "fixture result verified"])
.output()
.unwrap();
assert!(output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("worktree"), "{stderr}");
assert!(fixture.worktree(&task).is_dir());
drop(lock);
fixture.json(&["prune", "--apply"]);
assert!(!fixture.worktree(&task).exists());
}
#[test]
fn revise_after_a_rejection_starts_from_the_repository() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &[]);
let decision = fixture.json(&["reject", &task, "--reason", "try again"]);
assert_eq!(decision["verdict"], "rejected");
assert!(!fixture.worktree(&task).exists());
let revised = fixture.json(&["revise", &task, "clean again", "--foreground"]);
assert_eq!(revised["outcome"], "candidate", "{revised}");
}
#[test]
fn revise_ignores_a_kept_previous_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &["--keep-worktree"]);
fixture.json(&["reject", &task, "--reason", "try again"]);
assert!(fixture.worktree(&task).is_dir());
let revised = fixture.json(&["revise", &task, "clean again", "--foreground"]);
assert_eq!(revised["outcome"], "candidate", "{revised}");
}
#[test]
fn a_decision_waits_for_a_worker_that_is_still_exiting() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &[]);
let launched = Command::new("/bin/sh")
.args(["-c", "/bin/sleep 2 >/dev/null 2>&1 & echo $!"])
.output()
.unwrap();
let pid = String::from_utf8(launched.stdout).unwrap();
fs::write(
fixture.home.join("runs").join(format!("{task}.pid")),
pid.trim(),
)
.unwrap();
fixture.accept(&task);
assert!(
!fixture.worktree(&task).exists(),
"the decision skipped reclaiming while the worker exited"
);
}
#[test]
fn a_revision_keeps_its_worktree_when_the_task_asked_to() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &["--keep-worktree"]);
fixture.json(&["reject", &task, "--reason", "try again"]);
let revised = fixture.json(&["revise", &task, "clean again", "--foreground"]);
assert_eq!(revised["outcome"], "candidate", "{revised}");
fixture.accept(&task);
let second = fixture
.worktrees_root()
.join("repo")
.join(format!("{}-r2", Fixture::slug(&task)));
assert!(second.is_dir(), "the revision's worktree was reclaimed");
}
#[test]
fn keep_pane_does_not_keep_a_headless_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &["--keep-pane"]);
fixture.accept(&task);
assert!(!fixture.worktree(&task).exists());
}
#[test]
fn a_kept_worktree_is_noted_once_with_advice_accept_can_follow() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &[]);
fs::write(fixture.worktree(&task).join(".env"), b"TOKEN=secret\n").unwrap();
let output = fixture
.command()
.args(["accept", &task, "--reason", "fixture result verified"])
.output()
.unwrap();
assert!(output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(stderr.matches("kept worktree").count(), 1, "{stderr}");
assert!(
stderr.contains("brgr prune --apply --include-ignored"),
"{stderr}"
);
assert!(!stderr.contains("pass --include-ignored"), "{stderr}");
}
#[test]
fn a_decision_reads_only_its_own_repository() {
let fixture = Fixture::new();
let other = fixture.temp.path().join("other");
fs::create_dir_all(&other).unwrap();
git(&other, &["init", "-b", "main"]);
git(&other, &["config", "user.name", "Fixture"]);
git(&other, &["config", "user.email", "fixture@example.invalid"]);
fs::write(other.join("README"), b"other\n").unwrap();
git(&other, &["add", "README"]);
git(&other, &["commit", "-m", "seed"]);
fixture.json(&[
"run",
"clean",
"--workspace",
other.to_str().unwrap(),
"--foreground",
"--keep-worktree",
]);
let task = fixture.run_with("clean", &[]);
let shim = fixture.temp.path().join("shim");
fs::create_dir_all(&shim).unwrap();
let log = fixture.temp.path().join("git.log");
write_fast_script(
&shim.join("git"),
&format!(
"#!/bin/sh\necho \"$*\" >> '{}'\nexec /usr/bin/git \"$@\"\n",
log.display()
),
);
let path = format!("{}:{}", shim.display(), std::env::var("PATH").unwrap());
let output = fixture
.command()
.env("PATH", path)
.args(["accept", &task, "--reason", "fixture result verified"])
.output()
.unwrap();
assert!(output.status.success());
assert!(!fixture.worktree(&task).exists());
let calls = fs::read_to_string(&log).unwrap_or_default();
assert_eq!(
calls.matches("worktree list").count(),
1,
"a decision listed more than its own repository: {calls}"
);
}
#[test]
fn a_decision_made_from_inside_the_worktree_still_reclaims_it() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &[]);
let output = fixture
.command()
.current_dir(fixture.worktree(&task))
.args(["accept", &task, "--reason", "fixture result verified"])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!fixture.worktree(&task).exists(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn keep_worktree_keeps_it_after_the_decision() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &["--keep-worktree"]);
fixture.accept(&task);
assert!(fixture.worktree(&task).is_dir());
}
#[test]
fn a_worker_file_that_exists_nowhere_else_keeps_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &[]);
fixture.accept(&task);
assert!(fixture.worktree(&task).join("AGENT_OUTPUT.txt").is_file());
}
#[test]
fn a_worker_file_sealed_in_the_diff_does_not_keep_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &["--capture-diff"]);
fixture.accept(&task);
assert!(!fixture.worktree(&task).exists(), "sealed changes kept it");
let mut diff = fixture.command();
diff.args(["diff", &task]);
let output = diff.output().unwrap();
assert!(
String::from_utf8_lossy(&output.stdout).contains("AGENT_OUTPUT.txt"),
"the sealed diff lost the file"
);
let applied = fixture.json(&[
"apply",
&task,
"--workspace",
fixture.repo_arg(),
"--execute",
]);
assert_eq!(applied["status"], "applied", "{applied}");
assert_eq!(
fs::read_to_string(fixture.repository.join("AGENT_OUTPUT.txt")).unwrap(),
"worker output\n"
);
}
#[test]
fn a_change_made_after_sealing_keeps_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &["--capture-diff"]);
fs::write(
fixture.worktree(&task).join("AGENT_OUTPUT.txt"),
b"edited after the result was sealed\n",
)
.unwrap();
fixture.accept(&task);
assert!(fixture.worktree(&task).is_dir());
}
#[test]
fn an_evidence_file_changed_after_sealing_keeps_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with(
"WRITE",
&["--capture-diff", "--evidence-file", "AGENT_OUTPUT.txt"],
);
let evidence = fixture.worktree(&task).join("AGENT_OUTPUT.txt");
fs::write(&evidence, b"edited after the result was sealed\n").unwrap();
fixture.accept(&task);
assert!(
evidence.is_file(),
"an evidence file edited after sealing was deleted"
);
}
#[test]
fn an_unchanged_evidence_file_does_not_keep_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with(
"WRITE",
&["--capture-diff", "--evidence-file", "AGENT_OUTPUT.txt"],
);
fixture.accept(&task);
assert!(!fixture.worktree(&task).exists());
}
#[test]
fn a_nested_repository_keeps_the_worktree_even_with_a_matching_patch() {
let fixture = Fixture::new();
let task = fixture.run_with("NESTED", &["--capture-diff"]);
let work = fixture.worktree(&task).join("vendor/lib/work.txt");
assert!(work.is_file());
fixture.accept(&task);
assert!(work.is_file(), "the nested repository's work was deleted");
}
#[test]
fn an_unarchived_report_keeps_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &["--capture-diff"]);
let report = fixture
.worktree(&task)
.join(format!(".brgr/tasks/{task}-r1/report.md"));
fs::create_dir_all(report.parent().unwrap()).unwrap();
fs::write(&report, b"the worker's report\n").unwrap();
fixture.accept(&task);
assert!(report.is_file(), "an unarchived report was deleted");
}
#[test]
fn a_harness_cache_alone_does_not_keep_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("CACHE", &[]);
assert!(fixture.worktree(&task).join(".gjc").is_dir());
fixture.accept(&task);
assert!(!fixture.worktree(&task).exists());
}
#[test]
fn a_dotenv_alone_in_a_build_directory_keeps_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &[]);
let dist = fixture.worktree(&task).join("dist");
fs::create_dir_all(&dist).unwrap();
fs::write(dist.join(".env"), b"TOKEN=secret\n").unwrap();
fixture.accept(&task);
assert!(
dist.join(".env").is_file(),
"a secret inside dist/ was deleted with the worktree"
);
}
#[test]
fn a_new_claude_command_written_by_the_worker_keeps_the_worktree() {
let fixture = Fixture::new();
let task = fixture.run_with("clean", &[]);
let commands = fixture.worktree(&task).join(".claude/commands");
fs::create_dir_all(&commands).unwrap();
fs::write(commands.join("deploy.md"), b"# deploy\n").unwrap();
fixture.accept(&task);
assert!(
commands.join("deploy.md").is_file(),
"the worker's .claude/commands/deploy.md was deleted as a cache"
);
}
#[test]
fn a_dotenv_in_an_ignored_dist_directory_keeps_the_worktree() {
let fixture = Fixture::new();
let exclude = fixture.repository.join(".git/info/exclude");
fs::write(&exclude, b"dist/\n").unwrap();
let task = fixture.run_with("clean", &[]);
let dist = fixture.worktree(&task).join("dist");
fs::create_dir_all(&dist).unwrap();
fs::write(dist.join(".env"), b"TOKEN=secret\n").unwrap();
fixture.accept(&task);
assert!(
dist.join(".env").is_file(),
"a secret inside an ignored dist/ was deleted with the worktree"
);
}
#[test]
fn installed_dependencies_do_not_keep_the_worktree_but_a_dotenv_does() {
let fixture = Fixture::new();
let dependencies = fixture.run_with("clean", &[]);
let secret = fixture.run_with("clean", &[]);
let package = fixture.worktree(&dependencies).join("node_modules/pkg");
fs::create_dir_all(&package).unwrap();
fs::write(package.join("index.js"), b"module.exports = 1;\n").unwrap();
fs::write(fixture.worktree(&secret).join(".env"), b"TOKEN=secret\n").unwrap();
fixture.accept(&dependencies);
fixture.accept(&secret);
assert!(!fixture.worktree(&dependencies).exists());
assert!(fixture.worktree(&secret).join(".env").is_file());
}
#[test]
fn ignored_tool_state_directories_do_not_keep_the_worktree() {
let fixture = Fixture::new();
let exclude = fixture.repository.join(".git/info/exclude");
fs::write(&exclude, b".agent-progress/\n.build/\n.commandcode/\n").unwrap();
let task = fixture.run_with("clean", &[]);
let worktree = fixture.worktree(&task);
for name in [".agent-progress", ".build", ".commandcode"] {
let dir = worktree.join(name);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("state.json"), b"{}\n").unwrap();
}
fixture.accept(&task);
assert!(
!worktree.exists(),
"ignored tool-state directories kept the worktree"
);
}
#[test]
fn an_ignored_dotenv_among_tool_state_directories_keeps_the_worktree() {
let fixture = Fixture::new();
let exclude = fixture.repository.join(".git/info/exclude");
fs::write(&exclude, b".agent-progress/\n.build/\n.commandcode/\n").unwrap();
let task = fixture.run_with("clean", &[]);
let worktree = fixture.worktree(&task);
for name in [".agent-progress", ".build", ".commandcode"] {
let dir = worktree.join(name);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("state.json"), b"{}\n").unwrap();
}
fs::write(worktree.join(".env"), b"TOKEN=secret\n").unwrap();
fixture.accept(&task);
assert!(
worktree.join(".env").is_file(),
"an ignored .env among tool-state directories was deleted"
);
}
#[test]
fn lookalike_and_generic_ignored_paths_keep_the_worktree() {
let fixture = Fixture::new();
let exclude = fixture.repository.join(".git/info/exclude");
fs::write(&exclude, b".build-notes.txt\n.buildx/\ndist/\n.build\n").unwrap();
let notes = fixture.run_with("clean", &[]);
let extra = fixture.run_with("clean", &[]);
let dist = fixture.run_with("clean", &[]);
let file = fixture.run_with("clean", &[]);
fs::write(
fixture.worktree(¬es).join(".build-notes.txt"),
b"notes\n",
)
.unwrap();
let buildx = fixture.worktree(&extra).join(".buildx");
fs::create_dir_all(&buildx).unwrap();
fs::write(buildx.join("out"), b"x\n").unwrap();
let dist_dir = fixture.worktree(&dist).join("dist");
fs::create_dir_all(&dist_dir).unwrap();
fs::write(dist_dir.join("index.html"), b"<html></html>\n").unwrap();
fs::write(fixture.worktree(&file).join(".build"), b"not a directory\n").unwrap();
fixture.accept(¬es);
fixture.accept(&extra);
fixture.accept(&dist);
fixture.accept(&file);
assert!(
fixture.worktree(¬es).join(".build-notes.txt").is_file(),
".build-notes.txt was treated as disposable .build/"
);
assert!(
buildx.join("out").is_file(),
".buildx/ was treated as disposable .build/"
);
assert!(
dist_dir.join("index.html").is_file(),
"ignored dist/ was treated as disposable"
);
assert!(
fixture.worktree(&file).join(".build").is_file(),
"a file named .build was treated as a disposable directory"
);
}
#[test]
fn untracked_and_ignored_agent_progress_get_the_same_verdict() {
let fixture = Fixture::new();
let untracked = fixture.run_with("clean", &[]);
let exclude = fixture.repository.join(".git/info/exclude");
fs::write(&exclude, b".agent-progress/\n").unwrap();
let ignored = fixture.run_with("clean", &[]);
for task in [&untracked, &ignored] {
let dir = fixture.worktree(task).join(".agent-progress");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("state.json"), b"{}\n").unwrap();
}
fixture.accept(&untracked);
fixture.accept(&ignored);
assert!(
!fixture.worktree(&untracked).exists(),
"untracked .agent-progress/ kept the worktree"
);
assert!(
!fixture.worktree(&ignored).exists(),
"ignored .agent-progress/ kept the worktree"
);
}
#[test]
fn prune_reclaims_ignored_tool_state_directories_without_include_ignored() {
let fixture = Fixture::new();
let exclude = fixture.repository.join(".git/info/exclude");
fs::write(&exclude, b".agent-progress/\n.build/\n.commandcode/\n").unwrap();
let task = fixture.run_task("tools");
let worktree = fixture.worktree(&task);
for name in [".agent-progress", ".build", ".commandcode"] {
let dir = worktree.join(name);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("state.json"), b"{}\n").unwrap();
}
fixture.accept(&task);
assert!(
worktree.is_dir(),
"keep-worktree did not leave the checkout"
);
let report = fixture.prune(&[]);
assert_eq!(report["removable"], 1, "{report}");
let applied = fixture.prune(&["--apply"]);
assert_eq!(applied["removed"], 1, "{applied}");
assert!(!worktree.exists(), "prune left ignored tool-state behind");
}
#[test]
fn prune_removes_a_squashed_branch_whose_changes_the_store_sealed() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &["--capture-diff", "--keep-worktree"]);
fixture.commit_worktree(&task, "worker output");
fixture.accept(&task);
let contents = fs::read(fixture.worktree(&task).join("AGENT_OUTPUT.txt")).unwrap();
fixture.commit_on_main("AGENT_OUTPUT.txt", &contents, "squash");
assert!(
!fixture.tip_is_ancestor_of_head(&format!("brgr/task-{}", Fixture::slug(&task))),
"the squash simulation left the worker commit in HEAD"
);
let result_id = fixture.result_id(&task);
let applied = fixture.prune(&["--apply"]);
let branch = format!("brgr/task-{}", Fixture::slug(&task));
assert!(
!fixture.branches().contains(&branch),
"squashed sealed branch survived: {applied}"
);
let reason = reason_of(&applied, "sealed squash", &Fixture::slug(&task));
assert!(
reason.contains(&result_id),
"reason did not name the sealed result {result_id}: {reason}"
);
assert!(
reason.contains("sealed"),
"reason did not say the sealed result holds the changes: {reason}"
);
}
#[test]
fn prune_keeps_a_branch_with_changes_outside_the_sealed_diff() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &["--capture-diff", "--keep-worktree"]);
fixture.commit_worktree(&task, "worker output");
fixture.commit_in_worktree(&task);
fixture.accept(&task);
let applied = fixture.prune(&["--apply"]);
let branch = format!("brgr/task-{}", Fixture::slug(&task));
assert!(
fixture.branches().contains(&branch),
"a branch with extra unsealed commits was deleted: {applied}"
);
let reason = reason_of(&applied, "extra commit", &Fixture::slug(&task));
assert!(reason.contains("no other branch or remote"), "{reason}");
}
#[test]
fn prune_keeps_a_squashed_branch_when_the_task_has_no_sealed_diff() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &["--keep-worktree"]);
fixture.commit_worktree(&task, "worker output");
fixture.accept(&task);
let contents = fs::read(fixture.worktree(&task).join("AGENT_OUTPUT.txt")).unwrap();
fixture.commit_on_main("AGENT_OUTPUT.txt", &contents, "squash");
let applied = fixture.prune(&["--apply"]);
let branch = format!("brgr/task-{}", Fixture::slug(&task));
assert!(
fixture.branches().contains(&branch),
"a branch with no sealed diff was deleted: {applied}"
);
let reason = reason_of(&applied, "no sealed diff", &Fixture::slug(&task));
assert!(reason.contains("no other branch or remote"), "{reason}");
}
#[test]
fn prune_keeps_an_undecided_branch_even_when_its_changes_match_a_sealed_diff() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &["--capture-diff", "--keep-worktree"]);
fixture.commit_worktree(&task, "worker output");
let applied = fixture.prune(&["--apply"]);
assert!(fixture.worktree(&task).is_dir(), "{applied}");
assert!(
fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&task))),
"undecided branch was deleted: {applied}"
);
let reason = reason_of(&applied, "undecided", &Fixture::slug(&task));
assert!(reason.contains("decision"), "{reason}");
}
#[test]
fn prune_removes_a_rebased_branch_whose_changed_lines_are_a_subset_of_the_sealed_diff() {
let fixture = Fixture::new();
let task = fixture.run_with("EDIT", &["--capture-diff", "--keep-worktree"]);
fixture.commit_worktree(&task, "edit readme");
fixture.commit_on_main("README", b"header\nseed\n", "shift context");
git(&fixture.worktree(&task), &["rebase", "main"]);
fixture.accept(&task);
assert!(
!fixture.tip_is_ancestor_of_head(&format!("brgr/task-{}", Fixture::slug(&task))),
"the rebased tip is already in HEAD"
);
let applied = fixture.prune(&["--apply"]);
assert!(
!fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&task))),
"rebased sealed branch survived: {applied}"
);
let reason = reason_of(&applied, "rebased subset", &Fixture::slug(&task));
assert!(reason.contains("sealed"), "{reason}");
}
#[test]
fn prune_keeps_a_branch_whose_diff_includes_a_binary_file() {
let fixture = Fixture::new();
let task = fixture.run_with("BINARY", &["--capture-diff", "--keep-worktree"]);
fixture.commit_worktree(&task, "binary");
fixture.accept(&task);
let contents = fs::read(fixture.worktree(&task).join("image.bin")).unwrap();
fixture.commit_on_main("image.bin", &contents, "squash");
let applied = fixture.prune(&["--apply"]);
let branch = format!("brgr/task-{}", Fixture::slug(&task));
assert!(
fixture.branches().contains(&branch),
"a binary-diff branch was deleted without proving containment: {applied}"
);
let reason = reason_of(&applied, "binary", &Fixture::slug(&task));
assert!(reason.contains("no other branch or remote"), "{reason}");
}
#[test]
fn prune_counts_changed_line_multiplicity_against_the_sealed_diff() {
let fixture = Fixture::new();
let fewer = fixture.run_with("REPEAT", &["--capture-diff", "--keep-worktree"]);
fixture.commit_worktree(&fewer, "two foos");
fs::write(fixture.worktree(&fewer).join("repeat.txt"), b"keep\nfoo\n").unwrap();
fixture.commit_worktree(&fewer, "one foo");
fixture.accept(&fewer);
let fewer_bytes = fs::read(fixture.worktree(&fewer).join("repeat.txt")).unwrap();
fixture.commit_on_main("repeat.txt", &fewer_bytes, "squash fewer");
let extra = fixture.run_with("WRITE", &["--capture-diff", "--keep-worktree"]);
fs::write(
fixture.worktree(&extra).join("AGENT_OUTPUT.txt"),
b"worker output\nworker output\n",
)
.unwrap();
fixture.commit_worktree(&extra, "duplicated line");
fixture.accept(&extra);
let applied = fixture.prune(&["--apply"]);
assert!(
!fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&fewer))),
"a subset of the sealed multiplicity was kept: {applied}"
);
assert!(
fixture
.branches()
.contains(&format!("brgr/task-{}", Fixture::slug(&extra))),
"a branch that added a duplicate line was deleted: {applied}"
);
let extra_reason = reason_of(&applied, "extra multiplicity", &Fixture::slug(&extra));
assert!(
extra_reason.contains("no other branch or remote"),
"{extra_reason}"
);
}
struct Fixture {
temp: TempDir,
home: PathBuf,
repository: PathBuf,
}
impl Fixture {
fn new() -> Self {
let temp = TempDir::new().unwrap();
let home = temp.path().join("home");
let scratch = temp.path().join("scratch");
let repository = temp.path().join("repo");
fs::create_dir_all(&scratch).unwrap();
fs::create_dir_all(&repository).unwrap();
let fixture = Self {
temp,
home,
repository,
};
fixture.seed_repository();
fixture.register_harness(&scratch);
fixture
}
fn worktrees_root(&self) -> PathBuf {
self.home.join("worktrees")
}
fn worktree(&self, task: &str) -> PathBuf {
self.worktrees_root().join("repo").join(Fixture::slug(task))
}
fn slug(task: &str) -> String {
task[..8].to_owned()
}
fn run_task(&self, objective: &str) -> String {
self.run_with(objective, &["--keep-worktree"])
}
fn run_with(&self, objective: &str, extra: &[&str]) -> String {
let mut args = vec![
"run",
objective,
"--workspace",
self.repo_arg(),
"--foreground",
];
args.extend_from_slice(extra);
let receipt = self.json(&args);
assert_eq!(receipt["outcome"], "candidate");
receipt["task_id"].as_str().unwrap().to_owned()
}
fn run_failing_task(&self, objective: &str) -> String {
let objective = format!("SLOW {objective}");
let receipt = self.json(&[
"run",
&objective,
"--workspace",
self.repo_arg(),
"--deadline-seconds",
"1",
"--foreground",
"--keep-worktree",
]);
assert_ne!(receipt["outcome"], "candidate", "{receipt}");
receipt["task_id"].as_str().unwrap().to_owned()
}
fn acknowledge(&self, task: &str) {
self.json(&["result", task, "--ack"]);
}
fn accept(&self, task: &str) {
let decision = self.json(&["accept", task, "--reason", "fixture result verified"]);
assert_eq!(decision["verdict"], "accepted");
}
fn commit_on_new_branch(&self, name: &str) {
self.git(&["checkout", "-q", "-b", name]);
fs::write(
self.repository.join(format!("{name}.txt")),
b"feature work\n",
)
.unwrap();
self.git(&["add", "-A"]);
self.git(&["commit", "-q", "-m", "feature work"]);
}
fn commit_in_worktree(&self, task: &str) {
let worktree = self.worktree(task);
fs::write(worktree.join("AGENT_WORK"), b"produced by the run\n").unwrap();
git(&worktree, &["add", "-A"]);
git(&worktree, &["commit", "-m", "agent work"]);
}
fn commit_worktree(&self, task: &str, message: &str) {
let worktree = self.worktree(task);
git(&worktree, &["add", "-A"]);
git(&worktree, &["commit", "-m", message]);
}
fn commit_on_main(&self, relative: &str, contents: &[u8], message: &str) {
fs::write(self.repository.join(relative), contents).unwrap();
self.git(&["add", relative]);
self.git(&["commit", "-q", "-m", message]);
}
fn result_id(&self, task: &str) -> String {
self.json(&["result", task])["result"]["result_id"]
.as_str()
.unwrap()
.to_owned()
}
fn tip_is_ancestor_of_head(&self, branch: &str) -> bool {
Command::new("git")
.arg("-C")
.arg(&self.repository)
.args(["merge-base", "--is-ancestor", branch, "HEAD"])
.status()
.unwrap()
.success()
}
fn prune(&self, args: &[&str]) -> Value {
self.prune_in(self.home.clone(), args)
}
fn prune_in(&self, directory: PathBuf, args: &[&str]) -> Value {
let mut command = self.command();
command.current_dir(directory).arg("prune").args(args);
json(&command.output().unwrap())
}
fn json(&self, args: &[&str]) -> Value {
json(&self.command().args(args).output().unwrap())
}
fn repo_arg(&self) -> &str {
self.repository.to_str().unwrap()
}
fn command(&self) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_brgr"));
command
.arg("--headless")
.arg("--home")
.arg(&self.home)
.arg("--json")
.env_remove("CODEX_THREAD_ID")
.env_remove("CLAUDE_CODE_SESSION_ID")
.env_remove("BRGR_HOME")
.env_remove("BRGR_BIN")
.env_remove("BRGR_OWNER_ID")
.env_remove("BRGR_SESSION_ID")
.env_remove("BRGR_PARENT_TASK_ID")
.env_remove("BRGR_PARENT_ATTEMPT_ID")
.env_remove("BRGR_WORKER_HERDR_CONTEXT")
.env("BRGR_OWNER_ID", "codex:prune-owner")
.env("BRGR_SESSION_ID", "prune-session");
command
}
fn git(&self, args: &[&str]) {
git(&self.repository, args);
}
fn git_stdout(&self, args: &[&str]) -> String {
let output = Command::new("git")
.arg("-C")
.arg(&self.repository)
.args(args)
.output()
.unwrap();
assert!(output.status.success());
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn prune_counting_git(&self, args: &[&str]) -> Vec<String> {
let shim = self.temp.path().join("shim");
fs::create_dir_all(&shim).unwrap();
let log = self.temp.path().join("git-calls.log");
let _ = fs::remove_file(&log);
write_fast_script(
&shim.join("git"),
&format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {}\nexec /usr/bin/git \"$@\"\n",
log.display()
),
);
let path = format!(
"{}:{}",
shim.display(),
std::env::var("PATH").unwrap_or_default()
);
let output = self
.command()
.env("PATH", path)
.arg("prune")
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"prune failed: {}",
String::from_utf8_lossy(&output.stderr)
);
fs::read_to_string(&log)
.unwrap_or_default()
.lines()
.map(str::to_owned)
.filter(|line| !line.is_empty())
.collect()
}
fn forget_checkouts(&self) {
let store =
rusqlite::Connection::open(self.home.join("store").join("brgr.sqlite3")).unwrap();
store.execute("DELETE FROM task_checkouts", []).unwrap();
}
fn branches(&self) -> Vec<String> {
let output = Command::new("git")
.arg("-C")
.arg(&self.repository)
.args(["branch", "--format=%(refname:short)"])
.output()
.unwrap();
assert!(output.status.success());
String::from_utf8_lossy(&output.stdout)
.lines()
.map(|line| line.trim().to_owned())
.filter(|line| !line.is_empty())
.collect()
}
fn register_harness(&self, scratch: &Path) {
let output = self
.command()
.arg("harness")
.arg("add")
.arg(harness_fixture())
.arg("--workspace")
.arg(scratch)
.args(["--prompt", "BRGR_FIXTURE_OK"])
.output()
.unwrap();
assert!(
output.status.success(),
"fixture registration failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn seed_repository(&self) {
let repo = &self.repository;
git(repo, &["init", "-b", "main"]);
git(repo, &["config", "user.name", "Fixture"]);
git(repo, &["config", "user.email", "fixture@example.invalid"]);
fs::write(repo.join("README"), b"seed\n").unwrap();
fs::write(repo.join(".gitignore"), b".env\nnode_modules/\n").unwrap();
git(repo, &["add", "README", ".gitignore"]);
git(repo, &["commit", "-m", "seed"]);
git(repo, &["branch", "user/keep-me"]);
}
}
fn git(directory: &Path, args: &[&str]) {
let output = Command::new("git")
.arg("-C")
.arg(directory)
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn json(output: &std::process::Output) -> Value {
assert!(
output.status.success(),
"command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).unwrap()
}
fn harness_fixture() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../testdata/fixtures/gjc")
.canonicalize()
.unwrap()
}
#[test]
fn accept_keeps_the_task_branch_that_only_the_store_otherwise_holds() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &["--capture-diff"]);
fixture.commit_worktree(&task, "worker output");
fixture.accept(&task);
let branch = format!("brgr/task-{}", Fixture::slug(&task));
assert!(
!fixture.worktree(&task).exists(),
"the clean worktree should have been reclaimed at accept"
);
assert!(
fixture.branches().contains(&branch),
"accept deleted the task branch the owner may still push"
);
let applied = fixture.prune(&["--apply"]);
assert!(
!fixture.branches().contains(&branch),
"an explicit prune did not remove it: {applied}"
);
}
#[test]
fn prune_keeps_a_branch_whose_extra_commit_only_changes_a_file_mode() {
let fixture = Fixture::new();
let task = fixture.run_with("WRITE", &["--capture-diff", "--keep-worktree"]);
fixture.commit_worktree(&task, "worker output");
let file = fixture.worktree(&task).join("AGENT_OUTPUT.txt");
let mut permissions = fs::metadata(&file).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&file, permissions).unwrap();
fixture.commit_worktree(&task, "make it executable");
fixture.accept(&task);
let applied = fixture.prune(&["--apply"]);
let branch = format!("brgr/task-{}", Fixture::slug(&task));
assert!(
fixture.branches().contains(&branch),
"a branch with an unsealed mode change was deleted: {applied}"
);
}