#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::tests_outside_test_module,
reason = "integration test: fail-fast unwrap/expect are idiomatic, and test fns live at crate root by construction"
)]
use std::path::Path;
use std::process::{Command, Output};
const BIN: &str = env!("CARGO_BIN_EXE_doctrine");
fn git(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("spawn git");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn commit(dir: &Path, path: &str, content: &str, msg: &str) -> String {
let full = dir.join(path);
std::fs::create_dir_all(full.parent().unwrap()).unwrap();
std::fs::write(&full, content).unwrap();
git(dir, &["add", path]);
git(dir, &["commit", "-q", "-m", msg]);
git(dir, &["rev-parse", "HEAD"])
}
struct Fixture {
base: String,
}
fn build_fixture(dir: &Path) -> Fixture {
std::fs::create_dir_all(dir).unwrap();
git(dir, &["init", "-q", "-b", "main"]);
git(dir, &["config", "user.email", "t@example.com"]);
git(dir, &["config", "user.name", "Test"]);
let base = commit(dir, "trunk.txt", "trunk", "base");
git(dir, &["checkout", "-q", "-b", "dispatch/064"]);
let code_end_1 = commit(dir, "src1.txt", "a", "phase1 code");
let code_end_2 = commit(dir, "src2.txt", "b", "phase2 code");
commit(
dir,
".doctrine/slice/064/slice-064.md",
"scope",
"authored entity",
);
let boundaries = format!(
"[[boundary]]\nphase = \"PHASE-01\"\ncode_start_oid = \"{base}\"\ncode_end_oid = \"{code_end_1}\"\n\
[[boundary]]\nphase = \"PHASE-02\"\ncode_start_oid = \"{code_end_1}\"\ncode_end_oid = \"{code_end_2}\"\n"
);
std::fs::create_dir_all(dir.join(".doctrine/dispatch/064")).unwrap();
std::fs::write(
dir.join(".doctrine/dispatch/064/boundaries.toml"),
&boundaries,
)
.unwrap();
git(dir, &["add", ".doctrine/dispatch/064"]);
git(dir, &["commit", "-q", "-m", "ledger fixtures"]);
git(dir, &["checkout", "-q", "main"]);
Fixture { base }
}
fn run(cwd: &Path, worker: Option<bool>, args: &[&str]) -> Output {
let mut cmd = Command::new(BIN);
cmd.args(args).current_dir(cwd);
match worker {
Some(true) => {
cmd.env("DOCTRINE_WORKER", "1");
}
Some(false) | None => {
cmd.env_remove("DOCTRINE_WORKER");
}
}
cmd.output().expect("spawn doctrine")
}
fn stderr(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn ref_exists(dir: &Path, refname: &str) -> bool {
Command::new("git")
.arg("-C")
.arg(dir)
.args(["rev-parse", "--verify", "--quiet", refname])
.output()
.expect("spawn git")
.status
.success()
}
fn prepare_review(dir: &Path) {
let out = run(
dir,
None,
&[
"dispatch",
"sync",
"--prepare-review",
"--slice",
"64",
"-p",
dir.to_str().unwrap(),
],
);
assert!(
out.status.success(),
"prepare-review ok; stderr: {}",
stderr(&out)
);
}
fn create(cwd: &Path, worker: Option<bool>, extra: &[&str]) -> Output {
let mut args = vec!["dispatch", "candidate", "create", "--slice", "64"];
args.extend_from_slice(extra);
args.push("-p");
args.push(cwd.to_str().unwrap());
run(cwd, worker, &args)
}
fn read_candidates(dir: &Path) -> String {
std::fs::read_to_string(dir.join(".doctrine/dispatch/064/candidates.toml"))
.expect("candidates.toml written")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn status(cwd: &Path, worker: Option<bool>) -> Output {
run(
cwd,
worker,
&[
"dispatch",
"candidate",
"status",
"--slice",
"64",
"-p",
cwd.to_str().unwrap(),
],
)
}
fn parents(dir: &Path, commitish: &str) -> Vec<String> {
git(dir, &["rev-list", "--parents", "-n", "1", commitish])
.split_whitespace()
.skip(1)
.map(str::to_owned)
.collect()
}
fn admit(cwd: &Path, worker: Option<bool>, extra: &[&str]) -> Output {
let mut args = vec!["dispatch", "candidate", "admit", "--slice", "64"];
args.extend_from_slice(extra);
args.push("-p");
args.push(cwd.to_str().unwrap());
run(cwd, worker, &args)
}
fn integrate(cwd: &Path, extra: &[&str]) -> Output {
let mut args = vec![
"dispatch",
"sync",
"--integrate",
"--slice",
"64",
"-p",
cwd.to_str().unwrap(),
];
args.extend_from_slice(extra);
run(cwd, None, &args)
}
fn create_close_target(dir: &Path, label: &str) {
assert!(
create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
label,
"--source",
"refs/heads/phase/064-02",
],
)
.status
.success(),
"create close_target {label}"
);
}
#[test]
fn e2e_dispatch_candidate_clean_merge_from_review() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
let fx = build_fixture(dir);
prepare_review(dir);
let base_oid = git(dir, &["rev-parse", "main"]);
let source_oid = git(dir, &["rev-parse", "review/064"]);
let out = create(
dir,
None,
&[
"--role",
"review_surface",
"--payload",
"impl_bundle",
"--base",
"refs/heads/main",
"--label",
"review-001",
"--worktree",
],
);
assert!(
out.status.success(),
"clean create ok; stderr: {}",
stderr(&out)
);
assert!(
ref_exists(dir, "candidate/064/review-001"),
"branch created"
);
let merge_oid = git(dir, &["rev-parse", "candidate/064/review-001"]);
let ps = parents(dir, &merge_oid);
assert_eq!(ps.len(), 2, "no-ff merge has two parents: {ps:?}");
assert!(ps.contains(&base_oid), "base is a parent: {ps:?}");
assert!(ps.contains(&source_oid), "source is a parent: {ps:?}");
let toml = read_candidates(dir);
assert!(toml.contains("status = \"created\""), "{toml}");
assert!(toml.contains(&source_oid), "source_oid recorded: {toml}");
assert!(toml.contains(&base_oid), "base_oid recorded: {toml}");
assert!(toml.contains(&merge_oid), "merge_oid recorded: {toml}");
let diff = git(dir, &["diff", "--name-status", "main", &merge_oid]);
assert!(
!diff.lines().any(|l| l.starts_with('D')),
"no phantom deletions vs live trunk: {diff}"
);
let listing = git(dir, &["ls-tree", "-r", "--name-only", &merge_oid]);
assert!(
listing.contains(".doctrine/slice/064/slice-064.md"),
".doctrine corpus carried: {listing}"
);
assert!(
listing.contains("src1.txt"),
"source code carried: {listing}"
);
let _ = &fx.base;
}
#[test]
fn e2e_dispatch_candidate_clean_merge_from_phase_chain() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
let base_oid = git(dir, &["rev-parse", "main"]);
let source_oid = git(dir, &["rev-parse", "phase/064-02"]);
let out = create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"close-001",
"--source",
"refs/heads/phase/064-02",
],
);
assert!(
out.status.success(),
"phase-chain create ok; stderr: {}",
stderr(&out)
);
let merge_oid = git(dir, &["rev-parse", "candidate/064/close-001"]);
let ps = parents(dir, &merge_oid);
assert_eq!(ps.len(), 2, "no-ff merge has two parents: {ps:?}");
assert!(ps.contains(&base_oid) && ps.contains(&source_oid), "{ps:?}");
let diff = git(dir, &["diff", "--name-status", "main", &merge_oid]);
assert!(
!diff.lines().any(|l| l.starts_with('D')),
"no phantom deletions vs live trunk: {diff}"
);
}
#[test]
fn e2e_dispatch_candidate_unverified_source_refuses() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
let out = create(
dir,
None,
&[
"--role",
"review_surface",
"--payload",
"impl_bundle",
"--base",
"refs/heads/main",
"--label",
"review-001",
"--worktree",
],
);
assert!(
!out.status.success(),
"refused without a verified prepare-review row"
);
assert!(
stderr(&out).contains("prepare-review") || stderr(&out).contains("verified"),
"refusal names the provenance gate: {}",
stderr(&out)
);
assert!(
!ref_exists(dir, "candidate/064/review-001"),
"refused run creates no candidate ref"
);
assert!(
!dir.join(".doctrine/dispatch/064/candidates.toml").exists(),
"refused run records no row"
);
}
#[test]
fn e2e_dispatch_candidate_create_zero_oid_cas_refuses_existing() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
let mk = |label: &str, supersedes: Option<&str>| -> Output {
let mut extra = vec![
"--role",
"review_surface",
"--payload",
"impl_bundle",
"--base",
"refs/heads/main",
"--label",
label,
"--worktree",
];
if let Some(s) = supersedes {
extra.push("--supersedes");
extra.push(s);
}
create(dir, None, &extra)
};
assert!(mk("review-001", None).status.success(), "first create");
let first_oid = git(dir, &["rev-parse", "candidate/064/review-001"]);
let dup = mk("review-001", None);
assert!(!dup.status.success(), "existing target ref refuses");
assert!(
stderr(&dup).contains("already exists"),
"refusal names the CAS cause: {}",
stderr(&dup)
);
assert_eq!(
git(dir, &["rev-parse", "candidate/064/review-001"]),
first_oid,
"the existing branch is left untouched"
);
let sup = mk("review-002", Some("cand-064-review-001"));
assert!(
sup.status.success(),
"supersede create ok; stderr: {}",
stderr(&sup)
);
assert!(ref_exists(dir, "candidate/064/review-002"), "fresh ref");
let toml = read_candidates(dir);
assert!(
toml.contains("supersedes = \"cand-064-review-001\""),
"fresh row links the prior id: {toml}"
);
assert_eq!(
git(dir, &["rev-parse", "candidate/064/review-001"]),
first_oid,
"supersession never rewrites the old branch"
);
}
#[test]
fn e2e_dispatch_candidate_create_leaves_evidence_refs_and_distinguishes_payload() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
let review_before = git(dir, &["rev-parse", "review/064"]);
let phase1_before = git(dir, &["rev-parse", "phase/064-01"]);
let phase2_before = git(dir, &["rev-parse", "phase/064-02"]);
assert!(
create(
dir,
None,
&[
"--role",
"review_surface",
"--payload",
"impl_bundle",
"--base",
"refs/heads/main",
"--label",
"review-001",
"--worktree",
],
)
.status
.success()
);
assert!(
create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"close-001",
"--source",
"refs/heads/phase/064-02",
],
)
.status
.success()
);
assert_eq!(git(dir, &["rev-parse", "review/064"]), review_before);
assert_eq!(git(dir, &["rev-parse", "phase/064-01"]), phase1_before);
assert_eq!(git(dir, &["rev-parse", "phase/064-02"]), phase2_before);
let toml = read_candidates(dir);
assert!(toml.contains("payload = \"impl_bundle\""), "{toml}");
assert!(toml.contains("payload = \"code\""), "{toml}");
assert!(toml.contains("role = \"review_surface\""), "{toml}");
assert!(toml.contains("role = \"close_target\""), "{toml}");
let review_listing = git(
dir,
&["ls-tree", "-r", "--name-only", "candidate/064/review-001"],
);
let close_listing = git(
dir,
&["ls-tree", "-r", "--name-only", "candidate/064/close-001"],
);
assert!(
review_listing.contains(".doctrine/slice/064/slice-064.md"),
"impl_bundle candidate carries the .doctrine corpus: {review_listing}"
);
assert!(
!close_listing.contains(".doctrine/slice/064/slice-064.md")
|| review_listing != close_listing,
"code close_target differs from the impl_bundle review surface: \
review={review_listing} close={close_listing}"
);
}
fn build_conflict_fixture(dir: &Path) -> Fixture {
std::fs::create_dir_all(dir).unwrap();
git(dir, &["init", "-q", "-b", "main"]);
git(dir, &["config", "user.email", "t@example.com"]);
git(dir, &["config", "user.name", "Test"]);
let base = commit(dir, "trunk.txt", "trunk\n", "base");
git(dir, &["checkout", "-q", "-b", "dispatch/064"]);
let code_end_1 = commit(
dir,
"trunk.txt",
"DISPATCH SIDE\n",
"phase1 conflicting edit",
);
let code_end_2 = commit(dir, "src2.txt", "b", "phase2 code");
commit(
dir,
".doctrine/slice/064/slice-064.md",
"scope",
"authored entity",
);
let boundaries = format!(
"[[boundary]]\nphase = \"PHASE-01\"\ncode_start_oid = \"{base}\"\ncode_end_oid = \"{code_end_1}\"\n\
[[boundary]]\nphase = \"PHASE-02\"\ncode_start_oid = \"{code_end_1}\"\ncode_end_oid = \"{code_end_2}\"\n"
);
std::fs::create_dir_all(dir.join(".doctrine/dispatch/064")).unwrap();
std::fs::write(
dir.join(".doctrine/dispatch/064/boundaries.toml"),
&boundaries,
)
.unwrap();
git(dir, &["add", ".doctrine/dispatch/064"]);
git(dir, &["commit", "-q", "-m", "ledger fixtures"]);
git(dir, &["checkout", "-q", "main"]);
commit(dir, "trunk.txt", "MAIN SIDE\n", "main conflicting edit");
Fixture { base }
}
#[test]
fn e2e_dispatch_candidate_conflict_records_or_aborts() {
{
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_conflict_fixture(dir);
prepare_review(dir);
let out = create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"conflict-001",
"--source",
"refs/heads/review/064",
],
);
assert!(
!out.status.success(),
"conflict without --worktree aborts; stderr: {}",
stderr(&out)
);
assert!(
stderr(&out).contains("conflict"),
"refusal names the conflict: {}",
stderr(&out)
);
assert!(
!ref_exists(dir, "candidate/064/conflict-001"),
"aborted conflict creates no candidate ref"
);
assert!(
!dir.join(".doctrine/dispatch/064/candidates.toml").exists(),
"aborted conflict records no row"
);
assert!(
!dir.join(".doctrine/state/dispatch/candidate").exists(),
"aborted conflict leaves no worktree dir"
);
}
{
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_conflict_fixture(dir);
prepare_review(dir);
let base_oid = git(dir, &["rev-parse", "main"]);
let out = create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"conflict-001",
"--source",
"refs/heads/review/064",
"--worktree",
],
);
assert!(
out.status.success(),
"conflict with --worktree records the conflicted lifecycle; stderr: {}",
stderr(&out)
);
assert!(
ref_exists(dir, "candidate/064/conflict-001"),
"conflicted candidate branch created so the user can resolve"
);
assert_eq!(
git(dir, &["rev-parse", "candidate/064/conflict-001"]),
base_oid,
"conflicted branch parked at base for resolve+commit"
);
let toml = read_candidates(dir);
assert!(
toml.contains("status = \"conflicted\""),
"row recorded conflicted: {toml}"
);
let wt = dir.join(".doctrine/state/dispatch/candidate/cand-064-conflict-001");
assert!(wt.exists(), "conflicted candidate worktree created: {wt:?}");
assert!(
String::from_utf8_lossy(&out.stdout).contains("cand-064-conflict-001")
|| stderr(&out).contains("cand-064-conflict-001"),
"worktree path displayed; stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
stderr(&out)
);
}
}
#[test]
fn e2e_dispatch_candidate_review_surface_requires_worktree() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
let out = create(
dir,
None,
&[
"--role",
"review_surface",
"--payload",
"impl_bundle",
"--base",
"refs/heads/main",
"--label",
"review-001",
],
);
assert!(
!out.status.success(),
"review_surface without --worktree refuses"
);
assert!(
stderr(&out).contains("--worktree"),
"refusal names the missing flag: {}",
stderr(&out)
);
assert!(
!ref_exists(dir, "candidate/064/review-001"),
"refused review_surface creates no candidate ref"
);
assert!(
!dir.join(".doctrine/dispatch/064/candidates.toml").exists(),
"refused review_surface records no row"
);
}
#[test]
fn dispatch_candidate_is_orchestrator_classed() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
let out = create(
dir,
Some(true),
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"close-001",
"--source",
"refs/heads/phase/064-02",
],
);
assert!(!out.status.success(), "refused when DOCTRINE_WORKER set");
assert!(
stderr(&out).contains("DOCTRINE_WORKER"),
"carries the dual-cause token: {}",
stderr(&out)
);
assert!(
!ref_exists(dir, "candidate/064/close-001"),
"refused run creates no candidate ref"
);
assert!(
!dir.join(".doctrine/dispatch/064/candidates.toml").exists(),
"refused run records no row"
);
}
#[test]
fn e2e_dispatch_raw_evidence_worktree_write_refuses() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
git(dir, &["checkout", "-q", "review/064"]);
let out = create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"close-001",
"--source",
"refs/heads/phase/064-02",
],
);
assert!(
!out.status.success(),
"create refuses from a worktree on a raw evidence ref"
);
assert!(
stderr(&out).contains("review/064") || stderr(&out).contains("evidence"),
"refusal names the raw evidence ref: {}",
stderr(&out)
);
assert!(
stderr(&out).contains("candidate"),
"refusal points at the candidate workflow: {}",
stderr(&out)
);
assert!(
!ref_exists(dir, "candidate/064/close-001"),
"refused-on-evidence-ref run creates no candidate ref"
);
assert!(
!dir.join(".doctrine/dispatch/064/candidates.toml").exists(),
"refused-on-evidence-ref run records no row"
);
git(dir, &["checkout", "-q", "phase/064-01"]);
let out2 = create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"close-002",
"--source",
"refs/heads/phase/064-02",
],
);
assert!(
!out2.status.success(),
"create refuses from a worktree on a raw phase ref"
);
assert!(
stderr(&out2).contains("phase/064-01") || stderr(&out2).contains("evidence"),
"refusal names the raw phase ref: {}",
stderr(&out2)
);
}
#[test]
fn e2e_dispatch_candidate_create_refused_under_worker_mode() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
let out = create(
dir,
Some(true),
&[
"--role",
"review_surface",
"--payload",
"impl_bundle",
"--base",
"refs/heads/main",
"--label",
"review-001",
],
);
assert!(!out.status.success(), "refused when DOCTRINE_WORKER set");
assert!(
stderr(&out).contains("DOCTRINE_WORKER"),
"carries the dual-cause token: {}",
stderr(&out)
);
assert!(
!ref_exists(dir, "candidate/064/review-001"),
"refused run creates no candidate ref"
);
}
fn seed_close_target_admission(
dir: &Path,
candidate_id: &str,
candidate_ref: &str,
admitted_oid: &str,
) {
let path = dir.join(".doctrine/dispatch/064/candidates.toml");
let mut body = std::fs::read_to_string(&path).expect("candidates.toml exists");
body.push_str(&format!(
"\n[current_admission.close_target]\n\
candidate_id = \"{candidate_id}\"\n\
candidate_ref = \"{candidate_ref}\"\n\
expected_ref_oid = \"{admitted_oid}\"\n\
admitted_oid = \"{admitted_oid}\"\n\
review = \"RV-007\"\n\
admitted_at = \"2026-06-15\"\n"
));
std::fs::write(&path, body).unwrap();
}
#[test]
fn e2e_dispatch_candidate_status_groups_evidence_and_candidates() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
assert!(
create(
dir,
None,
&[
"--role",
"review_surface",
"--payload",
"impl_bundle",
"--base",
"refs/heads/main",
"--label",
"review-001",
"--worktree",
],
)
.status
.success()
);
assert!(
create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"close-001",
"--source",
"refs/heads/phase/064-02",
],
)
.status
.success()
);
let close_tip = git(dir, &["rev-parse", "candidate/064/close-001"]);
seed_close_target_admission(
dir,
"cand-064-close-001",
"refs/heads/candidate/064/close-001",
&close_tip,
);
let out = status(dir, None);
assert!(out.status.success(), "status ok; stderr: {}", stderr(&out));
let text = stdout(&out);
let ev = text.find("evidence refs:").expect("evidence group header");
let cd = text
.find("candidates (interaction branches):")
.expect("candidate group header");
assert!(
ev < cd,
"evidence group precedes the candidate group: {text}"
);
let evidence = &text[ev..cd];
assert!(
evidence.contains("refs/heads/dispatch/064") && evidence.contains("coordination"),
"evidence names the coordination branch: {evidence}"
);
assert!(
evidence.contains("refs/heads/review/064") && evidence.contains("impl-bundle"),
"evidence names the impl bundle: {evidence}"
);
assert!(
evidence.contains("refs/heads/phase/064-01")
&& evidence.contains("refs/heads/phase/064-02")
&& evidence.contains("phase-cut"),
"evidence names the phase cuts: {evidence}"
);
assert!(
!evidence.contains("candidate/064/"),
"the evidence group never lists a candidate interaction branch: {evidence}"
);
let candidates = &text[cd..];
assert!(
candidates.contains("cand-064-review-001") && candidates.contains("created"),
"the created review candidate is reported: {candidates}"
);
assert!(
candidates.contains("cand-064-close-001"),
"the admitted close candidate is reported: {candidates}"
);
assert!(
candidates.contains("admitted (RV-007)"),
"the admitted candidate names its admitting review: {candidates}"
);
let base = git(dir, &["rev-parse", "main"]);
assert!(
candidates.contains(&base[..12]),
"the base oid is reported: {candidates}"
);
}
#[test]
fn e2e_dispatch_candidate_status_reports_drift_when_ref_moves_past_admitted_oid() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
assert!(
create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"close-001",
"--source",
"refs/heads/phase/064-02",
],
)
.status
.success()
);
let admitted_oid = git(dir, &["rev-parse", "candidate/064/close-001"]);
seed_close_target_admission(
dir,
"cand-064-close-001",
"refs/heads/candidate/064/close-001",
&admitted_oid,
);
let before = stdout(&status(dir, None));
assert!(
before.contains("ok") && !before.contains("DRIFT"),
"no drift before the ref moves: {before}"
);
git(dir, &["checkout", "-q", "candidate/064/close-001"]);
let moved = commit(dir, "drifted.txt", "drift", "manual edit past admitted oid");
git(dir, &["checkout", "-q", "main"]);
assert_ne!(moved, admitted_oid, "the ref genuinely moved");
let after = stdout(&status(dir, None));
assert!(
after.contains("DRIFT"),
"the moved candidate ref is reported as drift: {after}"
);
let ledger = read_candidates(dir);
assert!(
ledger.contains(&admitted_oid),
"the admitted oid is unchanged after status: {ledger}"
);
assert!(
!ledger.contains(&moved),
"the moved tip was NOT written into the ledger (read-only): {ledger}"
);
}
#[test]
fn e2e_dispatch_candidate_status_names_evidence_interaction_and_next_action() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
assert!(
create(
dir,
None,
&[
"--role",
"review_surface",
"--payload",
"impl_bundle",
"--base",
"refs/heads/main",
"--label",
"review-001",
"--worktree",
],
)
.status
.success()
);
let text = stdout(&status(dir, None));
assert!(
text.contains("evidence refs:") && text.contains("refs/heads/review/064"),
"names the impl bundle as an evidence ref: {text}"
);
assert!(
text.contains("candidates (interaction branches):")
&& text.contains("candidate/064/review-001"),
"names the candidate as the interaction branch: {text}"
);
let next = text.split("next:").nth(1).expect("a next-action block");
assert!(
next.contains("dispatch candidate admit") || next.contains("dispatch candidate create"),
"the next block names a safe verb: {next}"
);
assert!(
!text.contains("git rev-parse") && !text.contains("git update-ref"),
"status guides via verbs, not raw ref plumbing: {text}"
);
}
#[test]
fn e2e_dispatch_candidate_status_empty_ledger_guides_to_create() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
let out = status(dir, None);
assert!(out.status.success(), "status ok; stderr: {}", stderr(&out));
let text = stdout(&out);
assert!(
text.contains("(none recorded)"),
"an empty ledger reports no candidates: {text}"
);
assert!(
text.contains("dispatch candidate create --slice 64"),
"the next action is to create the first candidate: {text}"
);
}
#[test]
fn e2e_dispatch_candidate_status_runs_under_worker_mode() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
let out = status(dir, Some(true));
assert!(
out.status.success(),
"status is read-only and runs under worker-mode; stderr: {}",
stderr(&out)
);
assert!(
stdout(&out).contains("evidence refs:"),
"status renders its surface under worker-mode"
);
}
#[test]
fn e2e_dispatch_candidate_admit_rejects_unproven_tip() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
create_close_target(dir, "close-001");
let merge_oid = git(dir, &["rev-parse", "candidate/064/close-001"]);
let main_oid = git(dir, &["rev-parse", "main"]);
let is_anc = Command::new("git")
.arg("-C")
.arg(dir)
.args(["merge-base", "--is-ancestor", &merge_oid, &main_oid])
.output()
.expect("spawn git")
.status
.success();
assert!(
!is_anc,
"the moved tip (main) genuinely does NOT descend from merge_oid"
);
git(dir, &["checkout", "-q", "candidate/064/close-001"]);
git(dir, &["reset", "--hard", "main"]);
git(dir, &["checkout", "-q", "main"]);
let out = admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
);
assert!(!out.status.success(), "admit refuses an unproven tip");
assert!(
stderr(&out).contains("descend") || stderr(&out).contains("I3"),
"refusal names the descent/I3 failure: {}",
stderr(&out)
);
let toml = read_candidates(dir);
assert!(
!toml.contains("[current_admission.close_target]"),
"no admission recorded on refusal: {toml}"
);
}
#[test]
fn e2e_dispatch_candidate_admit_records_immutable_oid_and_moved_ref_refuses() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
create_close_target(dir, "close-001");
let tip = git(dir, &["rev-parse", "candidate/064/close-001"]);
let out = admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
);
assert!(
out.status.success(),
"happy admit ok; stderr: {}",
stderr(&out)
);
let toml = read_candidates(dir);
assert!(
toml.contains("[current_admission.close_target]"),
"the close_target admission is recorded: {toml}"
);
assert!(
toml.contains(&format!("admitted_oid = \"{tip}\"")),
"admitted_oid pins the candidate tip: {toml}"
);
assert!(
toml.contains("candidate_id = \"cand-064-close-001\""),
"the admission names the candidate: {toml}"
);
assert!(
toml.contains("review = \"RV-007\""),
"the admission names its review: {toml}"
);
git(dir, &["checkout", "-q", "candidate/064/close-001"]);
let moved = commit(dir, "drifted.txt", "drift", "manual edit past admitted oid");
git(dir, &["checkout", "-q", "main"]);
assert_ne!(moved, tip, "the ref genuinely moved");
let after = read_candidates(dir);
assert!(
after.contains(&format!("admitted_oid = \"{tip}\"")),
"admitted_oid stays immutable after the ref moves: {after}"
);
assert!(
!after.contains(&moved),
"the moved tip is NOT written into the admission: {after}"
);
}
#[test]
fn e2e_dispatch_candidate_supersede_records_history() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
create_close_target(dir, "close-001");
assert!(
admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
)
.status
.success(),
"admit close-001"
);
assert!(
create(
dir,
None,
&[
"--role",
"close_target",
"--payload",
"code",
"--base",
"refs/heads/main",
"--label",
"close-002",
"--source",
"refs/heads/phase/064-02",
"--supersedes",
"cand-064-close-001",
],
)
.status
.success(),
"create close-002 superseding close-001"
);
let tip2 = git(dir, &["rev-parse", "candidate/064/close-002"]);
assert!(
admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-002",
"--review",
"RV-008",
],
)
.status
.success(),
"admit close-002"
);
let toml = read_candidates(dir);
assert_eq!(
toml.matches("[current_admission.close_target]").count(),
1,
"exactly one current close admission: {toml}"
);
assert!(
toml.contains("candidate_id = \"cand-064-close-002\""),
"the current admission is close-002: {toml}"
);
assert!(
toml.contains(&format!("admitted_oid = \"{tip2}\"")),
"admitted_oid is close-002's tip: {toml}"
);
assert!(
toml.contains("supersedes = \"cand-064-close-001\""),
"the admission supersedes the prior admitted candidate: {toml}"
);
}
#[test]
fn e2e_dispatch_candidate_admit_leaves_evidence_and_refused_under_worker() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
create_close_target(dir, "close-001");
let review_before = git(dir, &["rev-parse", "review/064"]);
let phase1_before = git(dir, &["rev-parse", "phase/064-01"]);
let phase2_before = git(dir, &["rev-parse", "phase/064-02"]);
let candidate_before = git(dir, &["rev-parse", "candidate/064/close-001"]);
assert!(
admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
)
.status
.success(),
"admit ok"
);
assert_eq!(git(dir, &["rev-parse", "review/064"]), review_before);
assert_eq!(git(dir, &["rev-parse", "phase/064-01"]), phase1_before);
assert_eq!(git(dir, &["rev-parse", "phase/064-02"]), phase2_before);
assert_eq!(
git(dir, &["rev-parse", "candidate/064/close-001"]),
candidate_before,
"the candidate ref itself is untouched"
);
let repo2 = tempfile::tempdir().unwrap();
let dir2 = repo2.path();
build_fixture(dir2);
prepare_review(dir2);
create_close_target(dir2, "close-001");
let out = admit(
dir2,
Some(true),
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
);
assert!(!out.status.success(), "refused when DOCTRINE_WORKER set");
assert!(
stderr(&out).contains("DOCTRINE_WORKER"),
"carries the dual-cause token: {}",
stderr(&out)
);
let toml = read_candidates(dir2);
assert!(
!toml.contains("[current_admission.close_target]"),
"no admission recorded under worker-mode refusal: {toml}"
);
}
fn dispatch_journal(dir: &Path) -> String {
git(
dir,
&["show", "dispatch/064:.doctrine/dispatch/064/journal.toml"],
)
}
#[test]
fn e2e_dispatch_candidate_admit_then_integrate_ff() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
create_close_target(dir, "close-001");
let admitted_oid = git(dir, &["rev-parse", "candidate/064/close-001"]);
assert!(
admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
)
.status
.success(),
"admit close-001"
);
let out = integrate(dir, &["--trunk", "refs/heads/main"]);
assert!(
out.status.success(),
"candidate-aware integrate --trunk ff; stderr: {}",
stderr(&out)
);
let main_after = git(dir, &["rev-parse", "main"]);
assert_eq!(
main_after, admitted_oid,
"trunk advanced to the admitted close_target OID, no close-time merge"
);
let journal = dispatch_journal(dir);
assert!(
journal.contains("target_ref = \"refs/heads/main\""),
"the trunk row targets main: {journal}"
);
assert!(
journal.contains(&format!("planned_new_oid = \"{admitted_oid}\"")),
"the trunk row's planned_new_oid is the admitted OID: {journal}"
);
assert!(
journal.contains(&format!("source_oid = \"{admitted_oid}\"")),
"the trunk row's source is the admitted OID (no merge synthesised): {journal}"
);
}
#[test]
fn e2e_dispatch_candidate_ref_moves_after_admit_close_uses_oid() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
create_close_target(dir, "close-001");
let admitted_oid = git(dir, &["rev-parse", "candidate/064/close-001"]);
assert!(
admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
)
.status
.success(),
"admit close-001"
);
git(dir, &["checkout", "-q", "candidate/064/close-001"]);
let moved_tip = commit(dir, "after.txt", "after admit", "move candidate past admit");
git(dir, &["checkout", "-q", "main"]);
assert_ne!(moved_tip, admitted_oid, "the candidate ref genuinely moved");
let out = integrate(dir, &["--trunk", "refs/heads/main"]);
assert!(
out.status.success(),
"integrate targets the admitted oid despite the moved ref; stderr: {}",
stderr(&out)
);
let main_after = git(dir, &["rev-parse", "main"]);
assert_eq!(
main_after, admitted_oid,
"trunk advanced to the ADMITTED oid (I4), not the moved candidate tip"
);
assert_ne!(
main_after, moved_tip,
"the moved candidate tip is NOT what integrate targeted"
);
}
#[test]
fn e2e_dispatch_candidate_trunk_moved_after_admit_refuses() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
create_close_target(dir, "close-001");
assert!(
admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
)
.status
.success(),
"admit close-001"
);
let main_advanced = commit(dir, "trunk-moved.txt", "moved", "trunk moves past admit");
let out = integrate(dir, &["--trunk", "refs/heads/main"]);
assert!(
!out.status.success(),
"integrate --trunk refuses a non-ff admitted oid"
);
let err = stderr(&out);
assert!(
err.contains("fast-forward") || err.contains("trunk moved"),
"refusal names the moved trunk: {err}"
);
assert!(
err.contains("superseding close-target") || err.contains("re-admit"),
"refusal instructs to create a superseding close-target candidate: {err}"
);
assert_eq!(
git(dir, &["rev-parse", "main"]),
main_advanced,
"trunk left at the post-advance tip — never clobbered"
);
}
#[test]
fn e2e_dispatch_candidate_integrate_edge_without_admission_refuses() {
let repo = tempfile::tempdir().unwrap();
let dir = repo.path();
build_fixture(dir);
prepare_review(dir);
create_close_target(dir, "close-001");
assert!(
admit(
dir,
None,
&[
"--role",
"close_target",
"--candidate",
"refs/heads/candidate/064/close-001",
"--review",
"RV-007",
],
)
.status
.success(),
"admit close-001"
);
let out = integrate(dir, &["--edge", "refs/heads/edge"]);
assert!(
!out.status.success(),
"integrate --edge refuses with no review_surface admission"
);
let err = stderr(&out);
assert!(
err.contains("review_surface"),
"refusal names the missing review_surface admission: {err}"
);
assert!(
!ref_exists(dir, "refs/heads/edge"),
"no silent raw-ref fallback — the edge ref was not created"
);
}