use serde_json::{Value, json};
use std::{
fs,
path::{Path, PathBuf},
process::{Command, Output},
};
struct Fixture {
_guard: tempfile::TempDir,
root: PathBuf,
}
impl Fixture {
fn path(&self) -> &Path {
&self.root
}
}
fn fixture() -> Fixture {
let guard = tempfile::Builder::new()
.prefix("shepherd-run-adoption-")
.tempdir()
.expect("create fixture root");
let root = fs::canonicalize(guard.path()).unwrap();
assert!(
Command::new("git")
.args(["init", "--quiet"])
.current_dir(&root)
.status()
.unwrap()
.success()
);
fs::create_dir_all(root.join(".shepherd/runs/v657/dispatch")).unwrap();
fs::write(
root.join(".shepherd/runs/v657/run.json"),
serde_json::to_vec(&json!({
"schema_version": 1, "run": "v657", "kind": "patch-arc", "branch": "v6.5.7",
"base": "main", "seed": "seed.md", "plan": "", "status": "planted", "lanes": [],
"updated_at": 42, "operator_note": {"preserve": true}
}))
.unwrap(),
)
.unwrap();
fs::write(
root.join(".shepherd/runs/v657/seed.md"),
b"legacy seed, preserve exactly\n",
)
.unwrap();
Fixture {
_guard: guard,
root,
}
}
fn invoke(root: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(args)
.current_dir(root)
.env("SHEPHERD_HOME", root.join("isolated-home"))
.output()
.unwrap()
}
fn git(root: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.args(args)
.current_dir(root)
.output()
.expect("run git fixture command");
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).trim().to_owned()
}
fn commit_all(root: &Path, message: &str) -> String {
git(root, &["add", "--all"]);
git(
root,
&[
"-c",
"user.name=Shepherd Tests",
"-c",
"user.email=shepherd-tests@example.invalid",
"commit",
"--quiet",
"-m",
message,
],
);
git(root, &["rev-parse", "HEAD"])
}
struct SuccessorFixture {
_guard: tempfile::TempDir,
root: PathBuf,
source_incarnation: String,
source_baseline: String,
target_baseline: String,
source_state: Vec<u8>,
source_seed: Vec<u8>,
source_plan: Vec<u8>,
source_dispatch: Vec<u8>,
}
impl SuccessorFixture {
fn new() -> Self {
let guard = tempfile::Builder::new()
.prefix("shepherd-run-successor-")
.tempdir()
.expect("create successor fixture root");
let root = fs::canonicalize(guard.path()).expect("canonical successor fixture root");
assert!(
Command::new("git")
.args(["init", "--quiet", "--initial-branch=main"])
.current_dir(&root)
.status()
.expect("initialize successor fixture git")
.success()
);
git(&root, &["config", "core.autocrlf", "false"]);
fs::write(root.join("product.txt"), b"baseline product\n").expect("baseline product");
let source_baseline = commit_all(&root, "baseline");
git(&root, &["switch", "--quiet", "-c", "v6.7.0"]);
let init = invoke(
&root,
&[
"run",
"init",
"v670",
"--kind",
"sprint",
"--branch",
"v6.7.0",
"--base",
"main",
"--version",
"6.7.0",
],
);
assert!(
init.status.success(),
"run init failed: {}",
String::from_utf8_lossy(&init.stderr)
);
let run_dir = root.join(".shepherd/runs/v670");
fs::write(run_dir.join("seed.md"), b"source seed evidence\n").expect("source seed");
fs::write(run_dir.join("plan.md"), b"source plan evidence\n").expect("source plan");
fs::write(run_dir.join("phase0.md"), b"source phase zero evidence\n")
.expect("source phase zero");
fs::write(
run_dir.join("plan-probes.json"),
serde_json::to_vec_pretty(&json!({
"schema": "shepherd.plan-probes/1",
"baseline": source_baseline,
"worktree_identity": "historical-source"
}))
.expect("encode source probes"),
)
.expect("source probes");
fs::write(
run_dir.join("dispatch/source-evidence.json"),
b"source dispatch evidence\n",
)
.expect("source dispatch evidence");
let mut state: Value = serde_json::from_slice(
&fs::read(run_dir.join("run.json")).expect("read initialized state"),
)
.expect("initialized state JSON");
state["seed"] = json!(".shepherd/runs/v670/seed.md");
state["plan"] = json!(".shepherd/runs/v670/plan.md");
fs::write(
run_dir.join("run.json"),
serde_json::to_vec_pretty(&state).expect("encode source state"),
)
.expect("source state");
let source_incarnation = state["run_incarnation"]
.as_str()
.expect("source incarnation")
.to_owned();
fs::write(root.join("product.txt"), b"descendant production change\n")
.expect("descendant product");
let target_baseline = commit_all(&root, "production descendant");
let source_state = fs::read(run_dir.join("run.json")).expect("source state bytes");
let source_seed = fs::read(run_dir.join("seed.md")).expect("source seed bytes");
let source_plan = fs::read(run_dir.join("plan.md")).expect("source plan bytes");
let source_dispatch =
fs::read(run_dir.join("dispatch/source-evidence.json")).expect("source dispatch bytes");
Self {
_guard: guard,
root,
source_incarnation,
source_baseline,
target_baseline,
source_state,
source_seed,
source_plan,
source_dispatch,
}
}
fn args(&self) -> Vec<String> {
vec![
"run".into(),
"successor".into(),
"v670".into(),
"--source-incarnation".into(),
self.source_incarnation.clone(),
"--version".into(),
"6.7.0".into(),
"--branch".into(),
"v6.7.0".into(),
"--base".into(),
"main".into(),
"--source-baseline".into(),
self.source_baseline.clone(),
"--baseline".into(),
self.target_baseline.clone(),
"--worktree".into(),
self.root.display().to_string(),
"--confirm".into(),
"--json".into(),
]
}
fn invoke(&self, args: &[String]) -> Output {
Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(args)
.current_dir(&self.root)
.env("SHEPHERD_HOME", self.root.join("isolated-home"))
.output()
.expect("invoke successor command")
}
}
fn state(root: &Path) -> Value {
serde_json::from_slice(&fs::read(root.join(".shepherd/runs/v657/run.json")).unwrap()).unwrap()
}
fn registry(root: &Path) -> Value {
serde_json::from_slice(
&fs::read(root.join(".shepherd/native-orientation-registry.json")).unwrap(),
)
.unwrap()
}
#[test]
fn explicit_legacy_adoption_preserves_artifacts_and_replays_one_native_identity() {
let temp = fixture();
let root = temp.path();
let original = state(root);
let seed = fs::read(root.join(".shepherd/runs/v657/seed.md")).unwrap();
let first = invoke(root, &["run", "migrate", "v657", "--adopt-native"]);
assert!(
first.status.success(),
"{}",
String::from_utf8_lossy(&first.stderr)
);
let adopted = state(root);
assert_eq!(adopted["status"], "planted");
assert_eq!(adopted["operator_note"], original["operator_note"]);
assert_eq!(adopted["updated_at"], original["updated_at"]);
assert_eq!(adopted["seed"], original["seed"]);
assert_eq!(adopted["orientation_epoch"], 0);
assert_eq!(adopted["run_incarnation"].as_str().unwrap().len(), 32);
let native = registry(root);
assert_eq!(
native["runs"]["v657"]["run_incarnation"],
adopted["run_incarnation"]
);
assert_eq!(
native["runs"]["v657"]["legacy_adoption_sha256"]
.as_str()
.unwrap()
.len(),
64
);
assert!(native["runs"]["v657"]["pre"].is_null());
assert!(native["runs"]["v657"]["post"].is_null());
assert_eq!(
seed,
fs::read(root.join(".shepherd/runs/v657/seed.md")).unwrap()
);
let second = invoke(root, &["run", "migrate", "v657", "--adopt-native"]);
assert!(
second.status.success(),
"{}",
String::from_utf8_lossy(&second.stderr)
);
assert_eq!(adopted, state(root));
assert_eq!(native, registry(root));
}
#[test]
fn adoption_cannot_import_execution_checkpoints_or_child_authority() {
for (field, value) in [
("status", json!("executing")),
("orientation_epoch", json!(1)),
("run_incarnation", json!("caller-chosen-incarnation")),
("lanes", json!([{"id":"old", "state":"pending"}])),
] {
let temp = fixture();
let mut document = state(temp.path());
document[field] = value;
let path = temp.path().join(".shepherd/runs/v657/run.json");
fs::write(&path, serde_json::to_vec(&document).unwrap()).unwrap();
let before = fs::read(&path).unwrap();
let result = invoke(temp.path(), &["run", "migrate", "v657", "--adopt-native"]);
assert!(!result.status.success(), "accepted {field}");
assert_eq!(before, fs::read(path).unwrap());
assert!(
!temp
.path()
.join(".shepherd/native-orientation-registry.json")
.exists()
);
}
for name in ["child.json", "pending-invalid.json"] {
let temp = fixture();
fs::write(
temp.path().join(".shepherd/runs/v657/dispatch").join(name),
b"{}",
)
.unwrap();
let before = state(temp.path());
let result = invoke(temp.path(), &["run", "migrate", "v657", "--adopt-native"]);
assert!(!result.status.success(), "accepted {name}");
assert_eq!(before, state(temp.path()));
assert!(
!temp
.path()
.join(".shepherd/native-orientation-registry.json")
.exists()
);
}
}
#[test]
fn ordinary_migration_does_not_adopt_and_bulk_adoption_is_refused() {
let temp = fixture();
let result = invoke(temp.path(), &["run", "migrate", "v657"]);
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
assert_eq!(state(temp.path())["run_incarnation"], "");
assert!(
!temp
.path()
.join(".shepherd/native-orientation-registry.json")
.exists()
);
let before = state(temp.path());
let bulk = invoke(temp.path(), &["run", "migrate", "--all", "--adopt-native"]);
assert!(!bulk.status.success());
assert_eq!(before, state(temp.path()));
assert!(
!temp
.path()
.join(".shepherd/native-orientation-registry.json")
.exists()
);
}
#[test]
fn native_adoption_intent_cannot_be_reused_for_changed_or_unrelated_authority() {
let temp = fixture();
let result = invoke(temp.path(), &["run", "migrate", "v657", "--adopt-native"]);
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
let before_native = registry(temp.path());
let path = temp.path().join(".shepherd/runs/v657/run.json");
let mut changed = state(temp.path());
changed["run_incarnation"] = json!("");
changed["branch"] = json!("different-source");
fs::write(&path, serde_json::to_vec(&changed).unwrap()).unwrap();
let result = invoke(temp.path(), &["run", "migrate", "v657", "--adopt-native"]);
assert!(!result.status.success());
assert_eq!(changed, state(temp.path()));
assert_eq!(before_native, registry(temp.path()));
let mut unrelated = before_native.clone();
unrelated["runs"]["v657"]
.as_object_mut()
.unwrap()
.remove("legacy_adoption_sha256");
let registry_path = temp
.path()
.join(".shepherd/native-orientation-registry.json");
fs::write(®istry_path, serde_json::to_vec(&unrelated).unwrap()).unwrap();
let result = invoke(temp.path(), &["run", "migrate", "v657", "--adopt-native"]);
assert!(!result.status.success());
assert_eq!(unrelated, registry(temp.path()));
}
#[test]
fn run_successor_archives_the_exact_source_and_mints_one_fresh_incarnation() {
let fixture = SuccessorFixture::new();
let first = fixture.invoke(&fixture.args());
assert!(
first.status.success(),
"successor failed: {}",
String::from_utf8_lossy(&first.stderr)
);
let report: Value = serde_json::from_slice(&first.stdout).expect("successor report JSON");
assert_eq!(report["schema"], "shepherd.run-successor/1");
assert_eq!(report["run"], "v670");
assert_eq!(report["source_incarnation"], fixture.source_incarnation);
let target_incarnation = report["target_incarnation"]
.as_str()
.expect("target incarnation");
assert_eq!(target_incarnation.len(), 32);
assert_ne!(target_incarnation, fixture.source_incarnation);
assert_eq!(report["version"], "6.7.0");
assert_eq!(report["branch"], "v6.7.0");
assert_eq!(report["base"], "main");
assert_eq!(report["base_commit"], fixture.source_baseline);
assert_eq!(report["source_baseline"], fixture.source_baseline);
assert_eq!(report["baseline"], fixture.target_baseline);
let active: Value = serde_json::from_slice(
&fs::read(fixture.root.join(".shepherd/runs/v670/run.json")).expect("successor run state"),
)
.expect("successor run JSON");
assert_eq!(active["run"], "v670");
assert_eq!(active["run_incarnation"], target_incarnation);
assert_eq!(active["status"], "planted");
assert_eq!(active["orientation_epoch"], 0);
assert_eq!(active["branch"], "v6.7.0");
assert_eq!(active["base"], "main");
assert_eq!(active["seed"], "");
assert_eq!(active["plan"], "");
assert_eq!(active["lanes"], json!([]));
let archive = PathBuf::from(
report["source_archive"]
.as_str()
.expect("source archive path"),
);
assert_eq!(
fixture.source_state,
fs::read(archive.join("run.json")).expect("archived source state")
);
assert_eq!(
fixture.source_seed,
fs::read(archive.join("seed.md")).expect("archived source seed")
);
assert_eq!(
fixture.source_plan,
fs::read(archive.join("plan.md")).expect("archived source plan")
);
assert_eq!(
fixture.source_dispatch,
fs::read(archive.join("dispatch/source-evidence.json"))
.expect("archived dispatch evidence")
);
assert!(fixture.root.join(".shepherd/runs/v670/dispatch").is_dir());
assert!(
fs::read_dir(fixture.root.join(".shepherd/runs/v670/dispatch"))
.expect("fresh successor dispatch directory")
.next()
.is_none(),
"source dispatch authority must not enter the successor"
);
for stale in ["seed.md", "plan.md", "phase0.md", "plan-probes.json"] {
assert!(
!fixture
.root
.join(".shepherd/runs/v670")
.join(stale)
.exists(),
"successor must not fabricate or reuse {stale}"
);
}
let native: Value = serde_json::from_slice(
&fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("native successor registry"),
)
.expect("native successor registry JSON");
assert_eq!(
native["runs"]["v670"]["run_incarnation"],
target_incarnation
);
assert_eq!(
native["runs"]["v670"]["predecessors"][0]["run_incarnation"],
fixture.source_incarnation
);
assert!(
native["pending_successors"]
.as_object()
.is_some_and(|v| v.is_empty())
);
let fresh_seed = include_str!("fixtures/seed-contract/valid/seed.md").replace("v999", "v670");
fs::write(fixture.root.join(".shepherd/runs/v670/seed.md"), fresh_seed)
.expect("fresh successor seed");
fs::write(
fixture.root.join(".shepherd/runs/v670/mesh.md"),
include_str!("fixtures/seed-contract/valid/mesh.md"),
)
.expect("fresh successor mesh");
let plant = invoke(
&fixture.root,
&[
"run",
"set",
"v670",
"--seed",
".shepherd/runs/v670/seed.md",
],
);
assert!(
plant.status.success(),
"ordinary successor planting failed: {}",
String::from_utf8_lossy(&plant.stderr)
);
let seed = invoke(
&fixture.root,
&["seed", "verify", ".shepherd/runs/v670/seed.md"],
);
assert!(
seed.status.success(),
"ordinary successor seed verification failed: {}",
String::from_utf8_lossy(&seed.stderr)
);
fs::write(archive.join("plan.md"), b"tampered historical plan\n")
.expect("tamper archived plan control");
let tampered = fixture.invoke(&fixture.args());
assert!(
!tampered.status.success(),
"tampered source archive replayed"
);
assert!(
String::from_utf8_lossy(&tampered.stderr).contains("archived source evidence changed"),
"archive tamper lacked causal diagnostic: {}",
String::from_utf8_lossy(&tampered.stderr)
);
fs::write(archive.join("plan.md"), &fixture.source_plan)
.expect("restore archived plan control");
let second = fixture.invoke(&fixture.args());
assert!(
second.status.success(),
"exact successor replay failed: {}",
String::from_utf8_lossy(&second.stderr)
);
let replay: Value = serde_json::from_slice(&second.stdout).expect("successor replay JSON");
assert_eq!(replay["target_incarnation"], target_incarnation);
assert_eq!(replay["source_archive"], report["source_archive"]);
let replay_native: Value = serde_json::from_slice(
&fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("replayed native successor registry"),
)
.expect("replayed native successor registry JSON");
assert_eq!(
replay_native, native,
"replay must mint no second successor"
);
}
#[test]
fn run_successor_rejects_every_identity_mismatch_without_mutation() {
let cases = [
(
"--source-incarnation",
"00000000000000000000000000000000",
"source incarnation",
),
("--version", "6.7.1", "version"),
("--branch", "wrong-branch", "branch"),
("--base", "wrong-base", "base"),
(
"--source-baseline",
"1111111111111111111111111111111111111111",
"source baseline",
),
(
"--baseline",
"2222222222222222222222222222222222222222",
"baseline",
),
];
for (flag, replacement, expected_diagnostic) in cases {
let fixture = SuccessorFixture::new();
let mut args = fixture.args();
let index = args.iter().position(|value| value == flag).expect("flag");
args[index + 1] = replacement.into();
let registry_before = fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("registry before denial");
let result = fixture.invoke(&args);
assert!(
!result.status.success(),
"identity mismatch {flag} unexpectedly succeeded"
);
assert!(
String::from_utf8_lossy(&result.stderr).contains(expected_diagnostic),
"identity mismatch {flag} lacked its causal diagnostic: {}",
String::from_utf8_lossy(&result.stderr)
);
assert_eq!(
fixture.source_state,
fs::read(fixture.root.join(".shepherd/runs/v670/run.json"))
.expect("source state after denial"),
"identity mismatch {flag} mutated source state"
);
assert_eq!(
registry_before,
fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("registry after denial"),
"identity mismatch {flag} mutated Native authority"
);
assert!(
!fixture
.root
.join(".shepherd/runs/v670/.incarnations")
.exists(),
"identity mismatch {flag} created archive state"
);
}
}
#[test]
fn run_successor_requires_confirmation_and_exact_selected_worktree() {
let fixture = SuccessorFixture::new();
let mut without_confirmation = fixture.args();
without_confirmation.retain(|value| value != "--confirm");
let denied = fixture.invoke(&without_confirmation);
assert!(
!denied.status.success(),
"successor ran without confirmation"
);
assert!(
String::from_utf8_lossy(&denied.stderr).contains("--confirm"),
"missing confirmation lacked its causal diagnostic: {}",
String::from_utf8_lossy(&denied.stderr)
);
assert_eq!(
fixture.source_state,
fs::read(fixture.root.join(".shepherd/runs/v670/run.json")).expect("unchanged state")
);
let unrelated = tempfile::tempdir().expect("unrelated worktree directory");
let mut wrong_worktree = fixture.args();
let index = wrong_worktree
.iter()
.position(|value| value == "--worktree")
.expect("worktree flag");
wrong_worktree[index + 1] = unrelated.path().display().to_string();
let denied = fixture.invoke(&wrong_worktree);
assert!(!denied.status.success(), "unrelated worktree was accepted");
assert!(
String::from_utf8_lossy(&denied.stderr).contains("worktree"),
"wrong worktree lacked its causal diagnostic: {}",
String::from_utf8_lossy(&denied.stderr)
);
assert_eq!(
fixture.source_state,
fs::read(fixture.root.join(".shepherd/runs/v670/run.json")).expect("unchanged state")
);
}
#[test]
fn run_successor_rejects_uncommitted_product_outside_run_evidence() {
let fixture = SuccessorFixture::new();
fs::write(
fixture.root.join("product.txt"),
b"uncommitted product drift\n",
)
.expect("dirty product control");
let registry_before = fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("registry before dirty denial");
let denied = fixture.invoke(&fixture.args());
assert!(
!denied.status.success(),
"dirty product worktree was accepted"
);
assert!(
String::from_utf8_lossy(&denied.stderr).contains("uncommitted")
&& String::from_utf8_lossy(&denied.stderr).contains("product"),
"dirty product denial lacked causal diagnostic: {}",
String::from_utf8_lossy(&denied.stderr)
);
assert_eq!(
fixture.source_state,
fs::read(fixture.root.join(".shepherd/runs/v670/run.json")).expect("unchanged source")
);
assert_eq!(
registry_before,
fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("registry after dirty denial")
);
}
#[test]
fn run_successor_rejects_ambiguous_native_history() {
let fixture = SuccessorFixture::new();
let first = fixture.invoke(&fixture.args());
assert!(
first.status.success(),
"initial successor failed: {}",
String::from_utf8_lossy(&first.stderr)
);
let registry_path = fixture
.root
.join(".shepherd/native-orientation-registry.json");
let mut native: Value =
serde_json::from_slice(&fs::read(®istry_path).expect("native history"))
.expect("native history JSON");
let duplicate = native["runs"]["v670"]["predecessors"][0].clone();
native["runs"]["v670"]["predecessors"]
.as_array_mut()
.expect("predecessor rows")
.push(duplicate);
fs::write(
®istry_path,
serde_json::to_vec_pretty(&native).expect("ambiguous Native history JSON"),
)
.expect("ambiguous Native history");
let before = fs::read(fixture.root.join(".shepherd/runs/v670/run.json"))
.expect("active state before ambiguity");
let denied = fixture.invoke(&fixture.args());
assert!(
!denied.status.success(),
"ambiguous Native history replayed"
);
assert!(
String::from_utf8_lossy(&denied.stderr).contains("ambiguous"),
"ambiguous history lacked causal diagnostic: {}",
String::from_utf8_lossy(&denied.stderr)
);
assert_eq!(
before,
fs::read(fixture.root.join(".shepherd/runs/v670/run.json"))
.expect("active state after ambiguity")
);
}
fn source_topology(root: &Path) -> Vec<(String, bool, u64)> {
fn visit(root: &Path, directory: &Path, rows: &mut Vec<(String, bool, u64)>) {
let mut entries = fs::read_dir(directory)
.expect("read topology directory")
.collect::<Result<Vec<_>, _>>()
.expect("read topology entries");
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let path = entry.path();
let metadata = fs::symlink_metadata(&path).expect("topology metadata");
let relative = path
.strip_prefix(root)
.expect("topology relative path")
.to_string_lossy()
.replace('\\', "/");
rows.push((relative, metadata.is_dir(), metadata.len()));
if metadata.is_dir() {
visit(root, &path, rows);
}
}
}
let mut rows = Vec::new();
visit(root, root, &mut rows);
rows
}
#[test]
fn run_successor_rejects_each_archive_bound_before_intent_or_move() {
for (case, expected) in [
("state-bytes", "exceeds"),
("probe-bytes", "exceeds"),
("entries", "maximum entry count"),
("depth", "maximum depth"),
("path", "maximum 512 bytes"),
("file-bytes", "maximum 16777216 bytes"),
("total-bytes", "maximum total"),
] {
let fixture = SuccessorFixture::new();
let run_dir = fixture.root.join(".shepherd/runs/v670");
match case {
"state-bytes" => {
fs::OpenOptions::new()
.write(true)
.open(run_dir.join("run.json"))
.expect("open oversized state")
.set_len(1024 * 1024 + 1)
.expect("size oversized state");
}
"probe-bytes" => {
fs::OpenOptions::new()
.write(true)
.open(run_dir.join("plan-probes.json"))
.expect("open oversized probes")
.set_len(1024 * 1024 + 1)
.expect("size oversized probes");
}
"entries" => {
let directory = run_dir.join("entry-overflow");
fs::create_dir(&directory).expect("entry overflow directory");
for index in 0..4097 {
fs::write(directory.join(format!("entry-{index:04}")), b"")
.expect("entry overflow file");
}
}
"depth" => {
let mut directory = run_dir.join("depth-overflow");
for _ in 0..17 {
directory.push("nested");
}
fs::create_dir_all(&directory).expect("depth overflow tree");
fs::write(directory.join("evidence"), b"deep").expect("deep evidence");
}
"path" => {
let segment = "p".repeat(180);
let directory = run_dir.join(&segment).join(&segment).join(&segment);
fs::create_dir_all(&directory).expect("path overflow tree");
fs::write(directory.join("evidence"), b"long path").expect("long path evidence");
}
"file-bytes" => {
fs::File::create(run_dir.join("oversized-evidence.bin"))
.expect("oversized evidence")
.set_len(16 * 1024 * 1024 + 1)
.expect("size oversized evidence");
}
"total-bytes" => {
let directory = run_dir.join("total-overflow");
fs::create_dir(&directory).expect("total overflow directory");
for index in 0..17 {
fs::File::create(directory.join(format!("chunk-{index:02}.bin")))
.expect("aggregate evidence")
.set_len(16 * 1024 * 1024)
.expect("size aggregate evidence");
}
}
_ => unreachable!(),
}
let state_before = fs::read(run_dir.join("run.json")).expect("state before bound denial");
let registry_path = fixture
.root
.join(".shepherd/native-orientation-registry.json");
let registry_before = fs::read(®istry_path).expect("registry before bound denial");
let topology_before = source_topology(&run_dir);
let denied = fixture.invoke(&fixture.args());
assert!(
!denied.status.success(),
"archive bound {case} was accepted"
);
assert!(
String::from_utf8_lossy(&denied.stderr).contains(expected),
"archive bound {case} lacked `{expected}`: {}",
String::from_utf8_lossy(&denied.stderr)
);
assert_eq!(
state_before,
fs::read(run_dir.join("run.json")).expect("state after bound denial"),
"archive bound {case} changed source state"
);
assert_eq!(
registry_before,
fs::read(®istry_path).expect("registry after bound denial"),
"archive bound {case} published Native intent"
);
assert_eq!(
topology_before,
source_topology(&run_dir),
"archive bound {case} moved source topology"
);
assert!(
!run_dir.join(".incarnations").exists(),
"archive bound {case} created an archive"
);
}
}
#[test]
fn run_successor_rejects_oversized_pending_registry_before_archive_move() {
let fixture = SuccessorFixture::new();
let registry_path = fixture
.root
.join(".shepherd/native-orientation-registry.json");
let mut native: Value =
serde_json::from_slice(&fs::read(®istry_path).expect("native registry"))
.expect("native registry JSON");
native["runs"]["v670"]["legacy_adoption_sha256"] = json!("a".repeat(130 * 1024));
fs::write(
®istry_path,
serde_json::to_vec_pretty(&native).expect("near-cap Native registry JSON"),
)
.expect("near-cap Native registry");
let registry_before = fs::read(®istry_path).expect("near-cap registry bytes");
let run_dir = fixture.root.join(".shepherd/runs/v670");
let topology_before = source_topology(&run_dir);
let denied = fixture.invoke(&fixture.args());
assert!(
!denied.status.success(),
"near-cap registry admitted successor"
);
assert!(
String::from_utf8_lossy(&denied.stderr).contains("exceeds its bounded reader"),
"near-cap denial lacked capacity diagnostic: {}",
String::from_utf8_lossy(&denied.stderr)
);
assert_eq!(
registry_before,
fs::read(®istry_path).expect("registry after capacity denial")
);
assert_eq!(topology_before, source_topology(&run_dir));
assert!(!run_dir.join(".incarnations").exists());
}
fn grow_unrelated_native_run_near_limit(registry_path: &Path) {
const TARGET: usize = 261_000;
let mut native: Value =
serde_json::from_slice(&fs::read(registry_path).expect("pending registry"))
.expect("pending registry JSON");
let mut unrelated = native["runs"]["v670"].clone();
unrelated["run"] = json!("v999");
unrelated["run_incarnation"] = json!("99999999999999999999999999999999");
unrelated["predecessors"] = json!([]);
unrelated["legacy_adoption_sha256"] = json!("");
native["runs"]["v999"] = unrelated;
let without_padding = serde_json::to_vec_pretty(&native).expect("near-limit registry shape");
assert!(without_padding.len() < TARGET);
native["runs"]["v999"]["legacy_adoption_sha256"] =
json!("r".repeat(TARGET - without_padding.len()));
let bytes = serde_json::to_vec_pretty(&native).expect("near-limit registry bytes");
assert!(
(260_900..262_144).contains(&bytes.len()),
"len={}",
bytes.len()
);
fs::write(registry_path, bytes).expect("near-limit unrelated run");
}
#[test]
fn run_successor_terminal_reservation_survives_near_cap_cross_run_growth() {
for abort in [false, true] {
let fixture = SuccessorFixture::new();
let run_dir = fixture.root.join(".shepherd/runs/v670");
let archive = run_dir
.join(".incarnations")
.join(&fixture.source_incarnation)
.join("source");
fs::create_dir_all(&archive).expect("near-cap collision archive");
fs::write(archive.join("plan.md"), b"near-cap collision\n").expect("near-cap collision");
let pending_result = fixture.invoke(&fixture.args());
assert!(
!pending_result.status.success(),
"collision must leave pending"
);
let registry_path = fixture
.root
.join(".shepherd/native-orientation-registry.json");
let pending: Value =
serde_json::from_slice(&fs::read(®istry_path).expect("pending registry"))
.expect("pending registry JSON");
let target = pending["pending_successors"]["v670"]["target_incarnation"]
.as_str()
.expect("pending target")
.to_owned();
assert_eq!(
pending["pending_successors"]["v670"]["terminal_reservation"]
.as_str()
.map(str::len),
Some(512)
);
grow_unrelated_native_run_near_limit(®istry_path);
if abort {
let result = invoke(
&fixture.root,
&[
"run",
"successor-abort",
"v670",
"--source-incarnation",
&fixture.source_incarnation,
"--target-incarnation",
&target,
"--confirm",
],
);
assert!(
result.status.success(),
"near-cap abort failed: {}",
String::from_utf8_lossy(&result.stderr)
);
assert_eq!(
fixture.source_state,
fs::read(run_dir.join("run.json")).expect("near-cap abort source")
);
} else {
fs::remove_file(archive.join("plan.md")).expect("clear near-cap collision");
let result = fixture.invoke(&fixture.args());
assert!(
result.status.success(),
"near-cap completion failed: {}",
String::from_utf8_lossy(&result.stderr)
);
let active: Value = serde_json::from_slice(
&fs::read(run_dir.join("run.json")).expect("near-cap successor state"),
)
.expect("near-cap successor JSON");
assert_eq!(active["run_incarnation"], target);
}
let terminal = fs::read(®istry_path).expect("near-cap terminal registry");
assert!(terminal.len() <= 256 * 1024, "len={}", terminal.len());
let terminal: Value = serde_json::from_slice(&terminal).expect("terminal registry JSON");
assert!(
terminal["pending_successors"]
.as_object()
.is_some_and(|v| v.is_empty())
);
}
}
#[test]
fn run_successor_replays_exact_preflight_failure_and_blocks_identity_drift() {
let fixture = SuccessorFixture::new();
let run_dir = fixture.root.join(".shepherd/runs/v670");
let archive = run_dir
.join(".incarnations")
.join(&fixture.source_incarnation)
.join("source");
fs::create_dir_all(&archive).expect("create partial archive root");
fs::write(archive.join("plan.md"), b"collision\n").expect("seed archive collision");
let first = fixture.invoke(&fixture.args());
assert!(
!first.status.success(),
"archive collision unexpectedly succeeded"
);
assert!(
String::from_utf8_lossy(&first.stderr).contains("archive destination already exists"),
"partial failure lacked causal archive diagnostic: {}",
String::from_utf8_lossy(&first.stderr)
);
assert_eq!(
fixture.source_seed,
fs::read(run_dir.join("seed.md")).expect("source seed stayed active")
);
assert_eq!(
fixture.source_plan,
fs::read(run_dir.join("plan.md")).expect("source plan stayed active")
);
assert_eq!(
fixture.source_dispatch,
fs::read(run_dir.join("dispatch/source-evidence.json"))
.expect("source dispatch stayed active")
);
let pending: Value = serde_json::from_slice(
&fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("pending Native registry"),
)
.expect("pending Native registry JSON");
let target_incarnation = pending["pending_successors"]["v670"]["target_incarnation"]
.as_str()
.expect("durable target incarnation")
.to_owned();
git(
&fixture.root,
&["branch", "--force", "main", &fixture.target_baseline],
);
let moved_base = fixture.invoke(&fixture.args());
assert!(!moved_base.status.success(), "moved base ref replayed");
assert!(
String::from_utf8_lossy(&moved_base.stderr).contains("base commit changed")
&& String::from_utf8_lossy(&moved_base.stderr).contains(&fixture.source_baseline)
&& String::from_utf8_lossy(&moved_base.stderr).contains(&fixture.target_baseline),
"moved base lacked exact expected/observed commits: {}",
String::from_utf8_lossy(&moved_base.stderr)
);
git(
&fixture.root,
&["branch", "--force", "main", &fixture.source_baseline],
);
let blocked = invoke(
&fixture.root,
&["run", "set", "v670", "--seed", "attacker.md"],
);
assert!(
!blocked.status.success(),
"mutation crossed pending successor"
);
assert!(
String::from_utf8_lossy(&blocked.stderr).contains("incomplete successor transition"),
"pending mutation denial lacked recovery route: {}",
String::from_utf8_lossy(&blocked.stderr)
);
fs::remove_file(archive.join("plan.md")).expect("remove injected collision");
let resumed = fixture.invoke(&fixture.args());
assert!(
resumed.status.success(),
"exact successor resume failed: {}",
String::from_utf8_lossy(&resumed.stderr)
);
let report: Value = serde_json::from_slice(&resumed.stdout).expect("resumed report JSON");
assert_eq!(report["target_incarnation"], target_incarnation);
assert_eq!(
fixture.source_plan,
fs::read(archive.join("plan.md")).expect("resumed archived source plan")
);
let active: Value = serde_json::from_slice(
&fs::read(fixture.root.join(".shepherd/runs/v670/run.json")).expect("resumed active state"),
)
.expect("resumed active state JSON");
assert_eq!(active["run_incarnation"], target_incarnation);
let native: Value = serde_json::from_slice(
&fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("completed Native registry"),
)
.expect("completed Native registry JSON");
assert!(
native["pending_successors"]
.as_object()
.is_some_and(|v| v.is_empty())
);
assert_eq!(
native["runs"]["v670"]["predecessors"]
.as_array()
.map(Vec::len),
Some(1)
);
}
#[test]
fn run_successor_abort_clears_only_exact_unactivated_pending_and_preserves_failure() {
let fixture = SuccessorFixture::new();
let run_dir = fixture.root.join(".shepherd/runs/v670");
let archive = run_dir
.join(".incarnations")
.join(&fixture.source_incarnation)
.join("source");
fs::create_dir_all(&archive).expect("create collision archive");
fs::write(archive.join("plan.md"), b"permanent collision\n")
.expect("permanent archive collision");
let source_before = fs::read(run_dir.join("run.json")).expect("source before abort");
let failed = fixture.invoke(&fixture.args());
assert!(
!failed.status.success(),
"permanent collision unexpectedly succeeded"
);
let pending: Value = serde_json::from_slice(
&fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("pending registry"),
)
.expect("pending registry JSON");
let target = pending["pending_successors"]["v670"]["target_incarnation"]
.as_str()
.expect("pending target")
.to_owned();
let wrong = invoke(
&fixture.root,
&[
"run",
"successor-abort",
"v670",
"--source-incarnation",
&fixture.source_incarnation,
"--target-incarnation",
"00000000000000000000000000000000",
"--confirm",
],
);
assert!(
!wrong.status.success(),
"wrong target aborted pending intent"
);
let aborted = invoke(
&fixture.root,
&[
"run",
"successor-abort",
"v670",
"--source-incarnation",
&fixture.source_incarnation,
"--target-incarnation",
&target,
"--confirm",
"--json",
],
);
assert!(
aborted.status.success(),
"exact successor abort failed: {}",
String::from_utf8_lossy(&aborted.stderr)
);
let report: Value = serde_json::from_slice(&aborted.stdout).expect("abort report JSON");
assert_eq!(report["schema"], "shepherd.run-successor-abort/1");
assert_eq!(report["source_incarnation"], fixture.source_incarnation);
assert_eq!(report["target_incarnation"], target);
assert_eq!(
source_before,
fs::read(run_dir.join("run.json")).expect("source after abort")
);
assert_eq!(
fs::read(archive.join("plan.md")).expect("preserved collision"),
b"permanent collision\n"
);
let native: Value = serde_json::from_slice(
&fs::read(
fixture
.root
.join(".shepherd/native-orientation-registry.json"),
)
.expect("aborted registry"),
)
.expect("aborted registry JSON");
assert!(
native["pending_successors"]
.as_object()
.is_some_and(|v| v.is_empty())
);
assert_eq!(
native["aborted_successors"]["v670"][0]["target_incarnation"],
target
);
let ordinary = invoke(
&fixture.root,
&["run", "set", "v670", "--seed", "fresh-after-abort.md"],
);
assert!(
ordinary.status.success(),
"abort did not restore ordinary mutation: {}",
String::from_utf8_lossy(&ordinary.stderr)
);
}
#[test]
fn run_successor_abort_recovers_post_rotation_pre_native_completion_crash() {
let fixture = SuccessorFixture::new();
let run_dir = fixture.root.join(".shepherd/runs/v670");
let archive = run_dir
.join(".incarnations")
.join(&fixture.source_incarnation)
.join("source");
fs::create_dir_all(&archive).expect("create successor archive");
fs::write(archive.join("plan.md"), b"prepare pending\n").expect("pending collision");
assert!(!fixture.invoke(&fixture.args()).status.success());
let registry_path = fixture
.root
.join(".shepherd/native-orientation-registry.json");
let pending_registry: Value =
serde_json::from_slice(&fs::read(®istry_path).expect("pending registry"))
.expect("pending registry JSON");
let pending = pending_registry["pending_successors"]["v670"].clone();
let target = pending["target_incarnation"]
.as_str()
.expect("pending target")
.to_owned();
fs::remove_file(archive.join("plan.md")).expect("clear pending collision");
let completed = fixture.invoke(&fixture.args());
assert!(
completed.status.success(),
"complete successor fixture: {}",
String::from_utf8_lossy(&completed.stderr)
);
let mut crashed: Value =
serde_json::from_slice(&fs::read(®istry_path).expect("completed registry"))
.expect("completed registry JSON");
crashed["runs"]["v670"] = pending["source_authority"].clone();
crashed["pending_successors"]["v670"] = pending;
fs::write(
®istry_path,
serde_json::to_vec_pretty(&crashed).expect("crash-seam registry JSON"),
)
.expect("crash-seam registry");
let abort_nonce = crashed["pending_successors"]["v670"]["abort_nonce"]
.as_str()
.expect("abort nonce");
let partial_staging = run_dir
.join(".incarnations")
.join(&fixture.source_incarnation)
.join(format!("abort-{abort_nonce}/restore-0"));
fs::create_dir_all(&partial_staging).expect("partial owned staging");
fs::write(partial_staging.join("run.json"), b"partial").expect("partial owned restored file");
fs::write(run_dir.join("dispatch/activated.json"), b"activated\n")
.expect("activated target control");
let refused = invoke(
&fixture.root,
&[
"run",
"successor-abort",
"v670",
"--source-incarnation",
&fixture.source_incarnation,
"--target-incarnation",
&target,
"--confirm",
],
);
assert!(!refused.status.success(), "activated target was aborted");
assert!(
String::from_utf8_lossy(&refused.stderr).contains("target lanes or dispatch authority"),
"activated-target denial lacked causal diagnostic: {}",
String::from_utf8_lossy(&refused.stderr)
);
fs::remove_file(run_dir.join("dispatch/activated.json"))
.expect("remove activated target control");
fs::write(run_dir.join("seed.md"), b"foreign final destination\n")
.expect("foreign final destination control");
let foreign = invoke(
&fixture.root,
&[
"run",
"successor-abort",
"v670",
"--source-incarnation",
&fixture.source_incarnation,
"--target-incarnation",
&target,
"--confirm",
],
);
assert!(
!foreign.status.success(),
"foreign final destination was overwritten"
);
assert!(
String::from_utf8_lossy(&foreign.stderr).contains("unowned target entry"),
"foreign destination lacked causal refusal: {}",
String::from_utf8_lossy(&foreign.stderr)
);
assert_eq!(
fs::read(run_dir.join("seed.md")).expect("preserved foreign destination"),
b"foreign final destination\n"
);
fs::remove_file(run_dir.join("seed.md")).expect("remove foreign destination control");
let aborted = invoke(
&fixture.root,
&[
"run",
"successor-abort",
"v670",
"--source-incarnation",
&fixture.source_incarnation,
"--target-incarnation",
&target,
"--confirm",
],
);
assert!(
aborted.status.success(),
"post-rotation abort failed: {}",
String::from_utf8_lossy(&aborted.stderr)
);
assert_eq!(
fixture.source_state,
fs::read(run_dir.join("run.json")).expect("restored source state")
);
assert_eq!(
fixture.source_seed,
fs::read(run_dir.join("seed.md")).expect("restored source seed")
);
assert_eq!(
fixture.source_plan,
fs::read(run_dir.join("plan.md")).expect("restored source plan")
);
assert_eq!(
fixture.source_state,
fs::read(archive.join("run.json")).expect("preserved archived source state")
);
let failed_target = run_dir
.join(".incarnations")
.join(&fixture.source_incarnation)
.join("failed-targets")
.join(&target);
let failed_state: Value = serde_json::from_slice(
&fs::read(failed_target.join("run.json")).expect("failed target state"),
)
.expect("failed target state JSON");
assert_eq!(failed_state["run_incarnation"], target);
assert_eq!(
fs::read(partial_staging.join("run.json")).expect("preserved partial staging"),
b"partial"
);
assert!(
fs::read_dir(failed_target.join("dispatch"))
.expect("failed target dispatch")
.next()
.is_none()
);
let native: Value =
serde_json::from_slice(&fs::read(®istry_path).expect("aborted crash-seam registry"))
.expect("aborted crash-seam registry JSON");
assert!(
native["pending_successors"]
.as_object()
.is_some_and(|v| v.is_empty())
);
assert_eq!(
native["runs"]["v670"]["run_incarnation"],
fixture.source_incarnation
);
}