mod common;
use std::{fs, path::Path};
use serde_json::Value;
use tempfile::TempDir;
use common::{brgr_command, calls, fake_herdr, herdr_env};
fn run(home: &Path, args: &[&str], envs: &[(&str, &str)]) -> std::process::Output {
let mut command = brgr_command();
command.arg("--home").arg(home).arg("--json").args(args);
if matches!(args.first(), Some(&"run")) {
command.arg("--headless");
}
if !envs.iter().any(|(name, _)| {
matches!(
*name,
"BRGR_SESSION_ID" | "CLAUDE_CODE_SESSION_ID" | "CURSOR_CONVERSATION_ID"
)
}) {
command.env("BRGR_SESSION_ID", "fixture-session");
}
for (name, value) in envs {
command.env(name, value);
}
command.output().unwrap()
}
fn json_output(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 brgr_fixture() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../testdata/fixtures/brgr-fixture")
.canonicalize()
.unwrap()
}
fn fixture_home() -> (TempDir, std::path::PathBuf, std::path::PathBuf) {
let temp = TempDir::new().unwrap();
let home = temp.path().join("brgr");
let workspace = temp.path().join("work");
fs::create_dir_all(&workspace).unwrap();
let scratch = temp.path().join("scratch");
fs::create_dir_all(&scratch).unwrap();
let receipt = json_output(&run(
&home,
&[
"harness",
"add",
brgr_fixture().to_str().unwrap(),
"--workspace",
scratch.to_str().unwrap(),
"--prompt",
"BRGR_FIXTURE_OK",
],
&[],
));
assert!(receipt["scratch_result_digest"].as_str().is_some());
(temp, home, workspace)
}
fn head_env() -> [(&'static str, &'static str); 2] {
[
("BRGR_OWNER_ID", "codex:head-takeover"),
("BRGR_SESSION_ID", "session-head"),
]
}
fn start_task(home: &Path, workspace: &Path) -> String {
let created = json_output(&run(
home,
&[
"run",
"BRGR_FIXTURE_OK",
"--workspace",
workspace.to_str().unwrap(),
"--foreground",
],
&head_env(),
));
created["task_id"].as_str().unwrap().to_owned()
}
fn takeover_count(home: &Path) -> i64 {
let database = home.join("store").join("brgr.sqlite3");
let connection = rusqlite::Connection::open(database).unwrap();
connection
.query_row("SELECT COUNT(*) FROM head_takeovers", [], |row| row.get(0))
.unwrap()
}
fn takeover_epoch(home: &Path, takeover_id: &str) -> u64 {
let store = brgr_store::Store::open(home.join("store")).unwrap();
store
.takeover(takeover_id.parse().unwrap())
.unwrap()
.unwrap()
.committed_epoch()
}
fn unrelated_env() -> [(&'static str, &'static str); 2] {
[
("BRGR_OWNER_ID", "codex:head-takeover"),
("BRGR_SESSION_ID", "session-unrelated"),
]
}
fn run_with(
home: &Path,
args: &[&str],
base: &[(&str, &str)],
extra: &[(&str, String)],
) -> std::process::Output {
let mut envs: Vec<(&str, &str)> = base.to_vec();
envs.extend(extra.iter().map(|(name, value)| (*name, value.as_str())));
run(home, args, &envs)
}
fn closed_panes(dir: &Path) -> Vec<String> {
calls(dir)
.lines()
.filter_map(|line| line.strip_prefix("pane close "))
.map(str::to_owned)
.collect()
}
fn offer_key(handoff: &Value) -> String {
handoff["offer"].as_str().unwrap().to_owned()
}
fn head_pane_env() -> [(&'static str, &'static str); 2] {
head_env()
}
fn incoming_session() -> [(&'static str, &'static str); 2] {
[
("BRGR_OWNER_ID", "codex:head-takeover"),
("BRGR_SESSION_ID", "session-incoming"),
]
}
fn incoming_session_without_brgr_session() -> [(&'static str, &'static str); 2] {
[
("BRGR_OWNER_ID", "codex:head-takeover"),
("CLAUDE_CODE_SESSION_ID", "session-incoming"),
]
}
fn start_handoff(home: &Path, task_id: &str, fake: &Path, dir: &Path, exit_old: bool) -> Value {
let mut args = vec![
"head",
"handoff",
task_id,
"--kind",
"claude",
"--goal",
"Finish the migration",
];
if exit_old {
args.push("--exit-old");
}
json_output(&run_with(
home,
&args,
&head_pane_env(),
&herdr_env(fake, dir, "p-old"),
))
}
fn start_owner_handoff(home: &Path, fake: &Path, dir: &Path, notes: Option<&str>) -> Value {
let mut args = vec![
"head",
"handoff",
"--kind",
"claude",
"--goal",
"Finish the migration",
];
if let Some(notes) = notes {
args.extend(["--notes", notes]);
}
json_output(&run_with(
home,
&args,
&head_pane_env(),
&herdr_env(fake, dir, "p-old"),
))
}
fn take_offer(home: &Path, fake: &Path, dir: &Path, task_id: Option<&str>, key: &str) -> Value {
let mut args = vec!["head", "take"];
if let Some(task_id) = task_id {
args.push(task_id);
}
args.extend(["--offer", key]);
json_output(&run_with(
home,
&args,
&incoming_session(),
&herdr_env(fake, dir, "p-new"),
))
}
#[test]
fn handoff_opens_a_pane_prompts_it_and_leaves_the_binding_with_the_outgoing_head() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let handoff = start_handoff(&home, &task_id, &fake, &dir, false);
assert_eq!(handoff["incoming_pane"], "p-new");
let log = calls(&dir);
assert!(log.contains("pane split"), "{log}");
assert!(log.contains("BRGR_SESSION_ID="), "{log}");
assert!(log.contains("--env BRGR_HOME="), "{log}");
assert!(log.contains("agent start head-"), "{log}");
let key = offer_key(&handoff);
assert!(
log.contains(&format!("head take {task_id} --offer {key}")),
"{log}"
);
assert_eq!(takeover_count(&home), 0);
let still_head = run(&home, &["result", &task_id], &head_env());
assert!(still_head.status.success());
}
#[test]
fn take_commits_for_the_incoming_session_and_tells_the_outgoing_pane() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let key = offer_key(&start_handoff(&home, &task_id, &fake, &dir, false));
let taken = json_output(&run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming_session(),
&herdr_env(&fake, &dir, "p-new"),
));
assert_eq!(taken["takeover"]["state"], "committed");
assert_eq!(
taken["binding_epoch"].as_u64().unwrap(),
takeover_epoch(&home, taken["takeover"]["takeover_id"].as_str().unwrap())
);
assert!(
calls(&dir).contains("agent prompt p-old"),
"{}",
calls(&dir)
);
let again = json_output(&run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming_session(),
&herdr_env(&fake, &dir, "p-new"),
));
assert_eq!(
again["takeover"]["takeover_id"],
taken["takeover"]["takeover_id"]
);
assert_eq!(again["binding_epoch"], taken["binding_epoch"]);
let stale = run(&home, &["result", &task_id, "--ack"], &head_env());
assert!(!stale.status.success());
}
#[test]
fn take_refuses_the_outgoing_session_an_unknown_offer_and_a_moved_head() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let key = offer_key(&start_handoff(&home, &task_id, &fake, &dir, false));
let herdr = herdr_env(&fake, &dir, "p-new");
let own = run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&head_env(),
&herdr,
);
assert!(!own.status.success());
let unknown = run_with(
&home,
&["head", "take", &task_id, "--offer", "no-such-offer"],
&incoming_session(),
&herdr,
);
assert!(!unknown.status.success());
let traversal = run_with(
&home,
&["head", "take", &task_id, "--offer", "../x"],
&incoming_session(),
&herdr,
);
assert!(!traversal.status.success());
assert_eq!(takeover_count(&home), 0);
json_output(&run(
&home,
&["head", "claim", &task_id, "--abandoned", "--goal", "g"],
&unrelated_env(),
));
let late = run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming_session(),
&herdr,
);
assert!(!late.status.success());
assert!(String::from_utf8_lossy(&late.stderr).contains("moved"));
}
#[test]
fn exit_old_closes_the_outgoing_pane_only_after_resume_and_a_clean_retire() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let key = offer_key(&start_handoff(&home, &task_id, &fake, &dir, true));
let herdr = herdr_env(&fake, &dir, "p-new");
let taken = json_output(&run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming_session(),
&herdr,
));
let takeover_id = taken["takeover"]["takeover_id"]
.as_str()
.unwrap()
.to_owned();
let early = run_with(
&home,
&["head", "retire", &takeover_id],
&incoming_session(),
&herdr,
);
assert!(!early.status.success());
assert!(closed_panes(&dir).is_empty(), "{}", calls(&dir));
json_output(&run_with(
&home,
&["accept", &task_id, "--reason", "incoming head acted"],
&incoming_session(),
&herdr,
));
let retired = json_output(&run_with(
&home,
&["head", "retire", &takeover_id],
&incoming_session(),
&herdr,
));
assert_eq!(retired["state"], "retired");
assert_eq!(retired["outgoing_head"]["closed"], true);
assert_eq!(closed_panes(&dir), ["p-old"], "{}", calls(&dir));
}
#[test]
fn a_busy_outgoing_head_is_not_closed_and_the_failure_is_reported_apart_from_the_handoff() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let key = offer_key(&start_handoff(&home, &task_id, &fake, &dir, true));
fs::write(dir.join("status-p-old"), "working").unwrap();
let herdr = herdr_env(&fake, &dir, "p-new");
let taken = json_output(&run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming_session(),
&herdr,
));
let takeover_id = taken["takeover"]["takeover_id"]
.as_str()
.unwrap()
.to_owned();
json_output(&run_with(
&home,
&["accept", &task_id, "--reason", "incoming head acted"],
&incoming_session(),
&herdr,
));
let retired = json_output(&run_with(
&home,
&["head", "retire", &takeover_id],
&incoming_session(),
&herdr,
));
assert_eq!(retired["state"], "retired");
assert_eq!(retired["outgoing_head"]["closed"], false);
assert!(closed_panes(&dir).is_empty(), "{}", calls(&dir));
fs::write(dir.join("status-p-old"), "idle").unwrap();
let exited = json_output(&run_with(
&home,
&["head", "exit", &takeover_id],
&incoming_session(),
&herdr,
));
assert_eq!(exited["outgoing_head"]["closed"], true);
assert_eq!(closed_panes(&dir), ["p-old"], "{}", calls(&dir));
}
#[test]
fn a_handoff_without_exit_old_never_closes_a_pane() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let key = offer_key(&start_handoff(&home, &task_id, &fake, &dir, false));
let herdr = herdr_env(&fake, &dir, "p-new");
let taken = json_output(&run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming_session(),
&herdr,
));
let takeover_id = taken["takeover"]["takeover_id"]
.as_str()
.unwrap()
.to_owned();
json_output(&run_with(
&home,
&["accept", &task_id, "--reason", "incoming head acted"],
&incoming_session(),
&herdr,
));
let retired = json_output(&run_with(
&home,
&["head", "retire", &takeover_id],
&incoming_session(),
&herdr,
));
assert!(retired.get("outgoing_head").is_none());
assert!(closed_panes(&dir).is_empty(), "{}", calls(&dir));
}
#[test]
fn the_incoming_prompt_states_the_owner_and_home_on_every_command() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
start_handoff(&home, &task_id, &fake, &dir, false);
let log = calls(&dir);
let prefix = "env -u BRGR_SESSION_ID -u BRGR_PARENT_TASK_ID -u BRGR_PARENT_ATTEMPT_ID BRGR_OWNER_ID='codex:head-takeover' BRGR_HOME='";
for command in ["head take", "status", "result", "head retire", "head exit"] {
let at = log
.find(&format!("{command} "))
.or_else(|| log.find(&format!("{command}`")))
.unwrap_or_else(|| panic!("no `{command}` in {log}"));
assert!(
log[..at]
.trim_end()
.rsplit('`')
.next()
.is_some_and(|run| run.contains(prefix)),
"`{command}` lacks the owner and home prefix: {log}"
);
}
}
#[test]
fn incoming_head_take_status_result_retire_work_without_brgr_session_id() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let key = offer_key(&start_handoff(&home, &task_id, &fake, &dir, false));
let herdr = herdr_env(&fake, &dir, "p-new");
let incoming = incoming_session_without_brgr_session();
let taken = json_output(&run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming,
&herdr,
));
json_output(&run_with(
&home,
&["status", &task_id, "--tree"],
&incoming,
&herdr,
));
json_output(&run_with(&home, &["result", &task_id], &incoming, &herdr));
json_output(&run_with(
&home,
&["accept", &task_id, "--reason", "incoming head acted"],
&incoming,
&herdr,
));
let takeover_id = taken["takeover"]["takeover_id"].as_str().unwrap();
json_output(&run_with(
&home,
&["head", "retire", takeover_id],
&incoming,
&herdr,
));
}
fn surface_pane(home: &Path, owner: &str) -> Option<(String, String)> {
let database = home.join("store").join("brgr.sqlite3");
let connection = rusqlite::Connection::open(database).unwrap();
connection
.query_row(
"SELECT session_id, pane_id FROM owner_surfaces WHERE owner_id = ?1",
[owner],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.ok()
}
#[test]
fn take_records_the_pane_the_handoff_opened_as_the_new_head_s_notification_pane() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let key = offer_key(&start_handoff(&home, &task_id, &fake, &dir, false));
assert_eq!(surface_pane(&home, "codex:head-takeover"), None);
let wrong = herdr_env(&fake, &dir, "p-someone-else");
json_output(&run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming_session(),
&wrong,
));
assert_eq!(
surface_pane(&home, "codex:head-takeover"),
Some(("session-incoming".to_owned(), "p-new".to_owned()))
);
}
#[test]
fn a_new_head_with_nothing_left_to_do_can_still_retire_the_handoff() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let key = offer_key(&start_handoff(&home, &task_id, &fake, &dir, true));
json_output(&run(
&home,
&["accept", &task_id, "--reason", "decided before the handoff"],
&head_env(),
));
let herdr = herdr_env(&fake, &dir, "p-new");
let taken = json_output(&run_with(
&home,
&["head", "take", &task_id, "--offer", &key],
&incoming_session(),
&herdr,
));
let takeover_id = taken["takeover"]["takeover_id"]
.as_str()
.unwrap()
.to_owned();
let retired = json_output(&run_with(
&home,
&["head", "retire", &takeover_id],
&incoming_session(),
&herdr,
));
assert_eq!(retired["state"], "retired");
assert_eq!(retired["outgoing_head"]["closed"], true);
assert_eq!(closed_panes(&dir), ["p-old"], "{}", calls(&dir));
}
#[test]
fn handoff_without_a_task_and_no_open_work_opens_a_pane_with_an_empty_manifest() {
let (temp, home, _workspace) = fixture_home();
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let handoff = start_owner_handoff(&home, &fake, &dir, Some("Keep the current plan"));
assert_eq!(handoff["incoming_pane"], "p-new");
let key = offer_key(&handoff);
assert!(
calls(&dir).contains(&format!("head take --offer {key}")),
"{}",
calls(&dir)
);
let taken = take_offer(&home, &fake, &dir, None, &key);
let manifest = &taken["manifest"];
assert_eq!(manifest["goal"], "Finish the migration");
assert_eq!(manifest["decision_notes"], "Keep the current plan");
assert_eq!(manifest["tasks"].as_array().map(Vec::len), Some(0));
}
#[test]
fn handoff_without_a_task_includes_every_open_task_in_the_manifest() {
let (temp, home, workspace) = fixture_home();
let first = start_task(&home, &workspace);
let second = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let handoff = start_owner_handoff(&home, &fake, &dir, None);
assert_eq!(handoff["incoming_pane"], "p-new");
let taken = take_offer(&home, &fake, &dir, None, &offer_key(&handoff));
let tasks: Vec<&str> = taken["manifest"]["tasks"]
.as_array()
.unwrap()
.iter()
.map(|task| task["task_id"].as_str().unwrap())
.collect();
assert!(tasks.contains(&first.as_str()), "{tasks:?}");
assert!(tasks.contains(&second.as_str()), "{tasks:?}");
assert_eq!(tasks.len(), 2, "{tasks:?}");
}
#[test]
fn handoff_with_an_explicit_task_still_names_that_task_in_the_take_prompt() {
let (temp, home, workspace) = fixture_home();
let task_id = start_task(&home, &workspace);
let dir = temp.path().join("fake");
fs::create_dir_all(&dir).unwrap();
let fake = fake_herdr(&dir);
let handoff = start_handoff(&home, &task_id, &fake, &dir, false);
assert_eq!(handoff["incoming_pane"], "p-new");
let key = offer_key(&handoff);
assert!(
calls(&dir).contains(&format!("head take {task_id} --offer {key}")),
"{}",
calls(&dir)
);
assert_eq!(takeover_count(&home), 0);
}
#[test]
fn a_malformed_handoff_task_id_hints_at_brgr_status() {
let output = brgr_command()
.args([
"head",
"handoff",
"wondermove-interview",
"--kind",
"claude",
"--goal",
"g",
])
.output()
.unwrap();
assert!(!output.status.success());
let err = String::from_utf8_lossy(&output.stderr);
assert!(err.contains("invalid character"), "{err}");
assert!(err.contains("brgr status"), "{err}");
}
#[test]
fn handoff_help_names_the_task_id_format() {
let output = brgr_command()
.args(["head", "handoff", "--help"])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let help = String::from_utf8_lossy(&output.stdout);
assert!(help.to_ascii_lowercase().contains("task id"), "{help}");
assert!(help.contains("brgr status"), "{help}");
assert!(
help.contains("open") && (help.contains("omit") || help.contains("Omit")),
"{help}"
);
}
#[test]
fn a_codex_handoff_without_a_visible_pane_says_to_run_a_short_command_first() {
let (_temp, home, _workspace) = fixture_home();
let session = "01a0fb33-447c-7ba0-9f37-ba04f33ce809";
let output = run(
&home,
&[
"--as", session, "head", "handoff", "--kind", "codex", "--goal", "g",
],
&[("BRGR_SESSION_ID", session)],
);
assert!(!output.status.success());
let said = String::from_utf8_lossy(&output.stderr);
assert!(said.contains("verified Herdr pane"), "{said}");
assert!(said.contains("status --brief"), "{said}");
}
#[test]
fn a_claude_handoff_without_a_pane_does_not_get_the_codex_advice() {
let (_temp, home, _workspace) = fixture_home();
let output = run(
&home,
&["head", "handoff", "--kind", "claude", "--goal", "g"],
&[("CLAUDE_CODE_SESSION_ID", "claude-session")],
);
assert!(!output.status.success());
let said = String::from_utf8_lossy(&output.stderr);
assert!(said.contains("verified Herdr pane"), "{said}");
assert!(!said.contains("status --brief"), "{said}");
}