mod support;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::{
fs,
path::{Path, PathBuf},
process::{Command, Output},
};
#[test]
fn candidate_freeze_is_a_native_command_with_exact_inputs() {
let output = Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(["candidate", "freeze", "--help"])
.output()
.expect("inspect native candidate command");
assert!(
output.status.success(),
"native candidate freeze is unavailable: {}",
String::from_utf8_lossy(&output.stderr)
);
let help = String::from_utf8(output.stdout).expect("UTF-8 help");
for flag in ["--run", "--source", "--manifest"] {
assert!(help.contains(flag), "candidate freeze is missing {flag}");
}
}
struct Fixture {
base: PathBuf,
root: PathBuf,
component: PathBuf,
manifest: PathBuf,
}
impl Fixture {
fn new() -> Self {
let nonce = uuid::Uuid::now_v7();
let base =
std::env::temp_dir().join(format!("shepherd-candidate-{}-{nonce}", std::process::id()));
fs::create_dir(&base).expect("fixture directory");
let base = fs::canonicalize(base).expect("canonical fixture");
let root = base.join("source");
fs::create_dir(&root).expect("source directory");
let fixture = Self {
component: base.join("component.wasm"),
manifest: base.join("product.json"),
base,
root,
};
fixture.git(&["init", "--quiet"]);
fixture.git(&["config", "user.name", "Candidate fixture"]);
fixture.git(&["config", "user.email", "candidate@example.invalid"]);
copy_tree(
&Path::new(env!("CARGO_MANIFEST_DIR")).join("../../content"),
&fixture.root.join("content"),
);
for path in [
"packages/scripts/test-active-adapters.mjs",
"packages/scripts/test-active-pi.mjs",
"scripts/prompts/packed-dogfood.md",
] {
let destination = fixture.root.join(path);
fs::create_dir_all(destination.parent().expect("fixture source parent"))
.expect("source parents");
fs::copy(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join(path),
destination,
)
.expect("candidate text source");
}
fs::write(fixture.root.join(".gitignore"), ".shepherd/project.json\n.shepherd/shepherd.db*\n.shepherd/shepherd.lock\n.shepherd/runs/**/run.json\n.shepherd/runs/**/run.lock\n.shepherd/runs/**/dispatch/\n").expect("fixture ignores");
fs::write(fixture.root.join("source.txt"), "product bytes\n").expect("product source");
fixture.success(&["init", "--confirm"]);
fixture.success(&[
"run",
"init",
"v657",
"--branch",
"v6.5.7",
"--base",
"main",
"--version",
"6.5.7",
]);
fixture.commit();
fs::write(&fixture.component, b"\0asm\x0d\0\x01\0").expect("fixture Component header");
fixture.measure();
fixture
}
fn invoke(&self, args: &[&str]) -> Output {
self.command(args)
.output()
.expect("native candidate command")
}
fn command(&self, args: &[&str]) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_shepherd"));
command
.args(args)
.current_dir(&self.root)
.env_clear()
.env("PATH", std::env::var_os("PATH").expect("PATH"))
.env("HOME", self.base.join("home"))
.env("SHEPHERD_HOME", self.base.join("home"));
command
}
fn success(&self, args: &[&str]) -> Output {
let result = self.invoke(args);
assert!(
result.status.success(),
"{args:?}: {}",
String::from_utf8_lossy(&result.stderr)
);
result
}
fn git(&self, args: &[&str]) -> String {
let result = Command::new("git")
.args(args)
.current_dir(&self.root)
.output()
.expect("fixture Git");
assert!(
result.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&result.stderr)
);
String::from_utf8(result.stdout)
.expect("UTF-8 Git")
.trim()
.into()
}
fn commit(&self) {
self.git(&["add", "-A"]);
self.git(&[
"commit",
"--quiet",
"--allow-empty",
"-m",
"fixture product",
]);
}
fn head(&self) -> String {
self.git(&["rev-parse", "HEAD"])
}
fn measure(&self) {
let output = self.success(&[
"candidate",
"manifest",
"--run",
"v657",
"--component",
self.component.to_str().expect("component path"),
"--json",
]);
fs::write(&self.manifest, output.stdout).expect("external product manifest");
}
fn freeze(&self) -> Output {
self.success(&[
"candidate",
"freeze",
"--run",
"v657",
"--source",
&self.head(),
"--manifest",
self.manifest.to_str().expect("manifest path"),
])
}
fn verify(&self, stage: &str) -> Output {
self.invoke(&[
"candidate",
"verify",
"--run",
"v657",
"--stage",
stage,
"--json",
])
}
fn record(&self) -> Value {
read_json(&self.root.join(".shepherd/runs/v657/run.json"))["candidate_custody"]["current"]
.clone()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
support::remove_dir_all(&self.base);
}
}
fn copy_tree(source: &Path, destination: &Path) {
fs::create_dir_all(destination).expect("content fixture directory");
for entry in fs::read_dir(source).expect("content directory") {
let entry = entry.expect("content entry");
let path = destination.join(entry.file_name());
if entry.file_type().expect("content type").is_dir() {
copy_tree(&entry.path(), &path);
} else {
fs::copy(entry.path(), path).expect("copy canonical content");
}
}
}
fn read_json(path: &Path) -> Value {
serde_json::from_slice(&fs::read(path).expect("fixture JSON")).expect("decode fixture JSON")
}
fn write_json(path: &Path, value: &Value) {
fs::write(
path,
serde_json::to_vec(value).expect("encode fixture JSON"),
)
.expect("write fixture JSON");
}
fn sha256(bytes: &[u8]) -> String {
Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn file_ref(path: &Path) -> Value {
let bytes = fs::read(path).expect("artifact bytes");
json!({"path": path, "sha256": sha256(&bytes), "bytes": bytes.len()})
}
fn tree_hash(root: &Path) -> String {
fn visit(
root: &Path,
directory: &Path,
files: &mut std::collections::BTreeMap<String, (u32, Vec<u8>)>,
) {
for entry in fs::read_dir(directory).expect("tree entries") {
let path = entry.expect("tree entry").path();
let metadata = fs::metadata(&path).expect("tree metadata");
if metadata.is_dir() {
visit(root, &path, files);
} else {
#[cfg(unix)]
let mode = {
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o777
};
#[cfg(not(unix))]
let mode = if metadata.permissions().readonly() {
0o444
} else {
0o666
};
files.insert(
path.strip_prefix(root)
.expect("relative path")
.to_str()
.expect("UTF-8")
.replace('\\', "/"),
(mode, fs::read(path).expect("tree bytes")),
);
}
}
}
let mut files = std::collections::BTreeMap::new();
visit(root, root, &mut files);
let mut hash = Sha256::new();
for (path, (mode, bytes)) in files {
hash.update(path);
hash.update([0]);
hash.update(mode.to_string());
hash.update([0]);
hash.update(bytes);
hash.update([0]);
}
hash.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
struct PackedFixture {
manifest: PathBuf,
installed: PathBuf,
lifecycle: PathBuf,
}
impl PackedFixture {
fn new(fixture: &Fixture) -> Self {
fixture.freeze();
assert!(fixture.verify("source").status.success());
fixture.success(&["candidate", "pack", "--run", "v657"]);
let bundle = fixture.base.join("packed");
fs::create_dir(&bundle).expect("packed fixture");
let mut candidate = serde_json::Map::new();
for (key, source) in [
("native_cli", PathBuf::from(env!("CARGO_BIN_EXE_shepherd"))),
("component_wasm", fixture.component.clone()),
(
"adapter_test",
fixture
.root
.join("packages/scripts/test-active-adapters.mjs"),
),
(
"adapter_pi_test",
fixture.root.join("packages/scripts/test-active-pi.mjs"),
),
(
"dogfood_prompt",
fixture.root.join("scripts/prompts/packed-dogfood.md"),
),
] {
let target = bundle.join(key);
fs::copy(source, &target).expect("packed candidate copy");
candidate.insert(key.into(), file_ref(&target));
}
let javascript = bundle.join("component_js");
fs::write(&javascript, "export const fixture = true;\n").expect("fixture generated JS");
candidate.insert("component_js".into(), file_ref(&javascript));
let installed = bundle.join("node_modules");
fs::create_dir_all(installed.join("@pzzld/component-runtime/runtime"))
.expect("installed runtime");
fs::copy(
&javascript,
installed.join("@pzzld/component-runtime/runtime/shepherd-component.js"),
)
.expect("installed JS");
fs::copy(
&fixture.component,
installed.join("@pzzld/component-runtime/runtime/shepherd-component.wasm"),
)
.expect("installed Component");
let mut packages = serde_json::Map::new();
for name in [
"component-runtime",
"claude-shepherd",
"codex-shepherd",
"pi-shepherd",
] {
let package = format!("@pzzld/{name}");
let destination = installed.join(&package);
fs::create_dir_all(&destination).expect("installed package directory");
write_json(
&destination.join("package.json"),
&json!({"name": package, "version": env!("CARGO_PKG_VERSION")}),
);
if name != "component-runtime" {
fixture.success(&[
"compile",
"--target",
name.strip_suffix("-shepherd").expect("harness"),
"--out",
destination.to_str().expect("compile destination"),
]);
}
let archive = bundle.join(format!("{name}.tgz"));
fs::write(
&archive,
format!("byte-custody fixture archive for {name}\n"),
)
.expect("fixture archive");
packages.insert(
package,
json!({"version": env!("CARGO_PKG_VERSION"), "archive": file_ref(&archive)}),
);
}
let marketplace = bundle.join("marketplace");
let claude = marketplace.join("plugins/shepherd");
copy_tree(&installed.join("@pzzld/claude-shepherd"), &claude);
copy_tree(
&installed.join("@pzzld/codex-shepherd/.agents/skills"),
&claude.join("codex/skills"),
);
let manifest = bundle.join("packages.json");
write_json(
&manifest,
&json!({
"schema": "shepherd.packed-dogfood-input/1", "candidate_id": fixture.record()["candidate_id"], "version": env!("CARGO_PKG_VERSION"),
"source": {"root": fixture.root, "commit": fixture.head(), "tree": fixture.git(&["rev-parse", "HEAD^{tree}"]), "accepted_base": fixture.head()},
"candidate": candidate, "packages": packages,
"installed": {"root": installed, "sha256": tree_hash(&installed)},
"carriers": {"claude": {"root": claude, "sha256": tree_hash(&claude)}, "codex": {"root": marketplace, "sha256": tree_hash(&marketplace)},
"pi": {"package": "@pzzld/pi-shepherd", "sha256": packages["@pzzld/pi-shepherd"]["archive"]["sha256"]}}
}),
);
Self {
manifest,
installed,
lifecycle: bundle.join("lifecycle.json"),
}
}
fn attest(&self, fixture: &Fixture, kind: &str) -> Output {
let path = if kind == "packages" {
&self.manifest
} else {
&self.lifecycle
};
fixture.invoke(&[
"candidate",
"attest",
"--run",
"v657",
"--kind",
kind,
"--manifest",
path.to_str().expect("attestation path"),
])
}
fn lifecycle(&self, fixture: &Fixture) {
let packages = read_json(&self.manifest);
let matrix = fixture.base.join("matrix");
fs::create_dir(&matrix).expect("matrix directory");
let mut hosts = Vec::new();
for harness in ["claude", "codex", "pi"] {
let project = matrix.join(harness).join("project");
let session = matrix.join(harness).join("session");
let home = matrix.join(harness).join("home");
let cache = matrix.join(harness).join("cache");
for path in [&project, &session, &home, &cache] {
fs::create_dir_all(path).expect("fresh host root");
}
let artifact = project.join("native-result.json");
fs::write(&artifact, "{\"fixture\":true}\n").expect("native artifact");
let traces = session.join("machine-evidence");
fs::create_dir(&traces).expect("machine evidence");
let mut cases = vec!["project-init", "user-home-init", "run-init", "plant"];
if harness == "pi" {
cases.extend([
"engineer-orientation",
"concurrent-auditor-discovery",
"critic-revision",
"nested-project-run-custody",
]);
}
cases.extend(["plan-verify", "planned", "start"]);
if harness == "codex" {
cases.push("hook-guard");
}
cases.extend([
"bounded-allow",
"bounded-overflow-deny",
"review-redo-1",
"review-redo-2",
"review-redo-3",
"review-rejection-4",
"review-terminal-denials",
"read-only-review",
"dispatch",
"report",
"compaction",
"stop",
"resume",
"close",
]);
let mut proofs = Vec::new();
let mut previous = "0".repeat(64);
for (sequence, case) in cases.iter().enumerate() {
let denied = (harness == "codex"
&& ["dispatch", "report", "compaction"].contains(case))
|| (harness == "pi" && *case == "compaction");
let status = if denied {
"observed-fail-closed"
} else {
"passed"
};
let exit = if denied { 1 } else { 0 };
let command = json!([
packages["candidate"]["native_cli"]["path"],
"fixture-command",
case
]);
let mut payload = json!({
"schema": "shepherd.packed-dogfood-machine-case/5", "harness": harness, "run": format!("dogfood-{harness}"),
"selection": {"run": "v657", "phase": "planning"}, "case": case, "status": status, "exit": exit,
"sequence": sequence, "previous_proof_sha256": previous, "recipe_sha256": sha256(case.as_bytes()), "parameters": {},
"manifest_sha256": file_ref(&self.manifest)["sha256"], "candidate_sha256": packages["candidate"]["native_cli"]["sha256"],
"cwd": project, "state_before_sha256": "1".repeat(64), "state_after_sha256": "2".repeat(64), "verified_lane_input": null,
"commands": [{"command": command, "exit": exit, "stdin_sha256": null, "stdout_sha256": "3".repeat(64), "stderr_sha256": "4".repeat(64), "stdout_bytes": 0, "stderr_bytes": 0}]
});
if [
"review-redo-1",
"review-redo-2",
"review-redo-3",
"review-rejection-4",
]
.contains(case)
{
payload["review_checkpoint"] = json!({"schema": "shepherd.packed-review-checkpoint/1", "round": case.chars().last().expect("round").to_digit(10).expect("round number"), "request": {}, "before": {}, "after": {}, "retained_artifacts": []});
}
if *case == "review-terminal-denials" {
payload["terminal_verification"] = json!({"schema": "shepherd.review-terminal-verification/1", "proof_kind": "native-prelaunch-predicate-denials", "broker_peer_attempted": false, "provider_launch_attempted": false, "subject_mutated": false});
}
let path = traces.join(format!("{case}.json"));
write_json(&path, &json!({"payload": payload, "hmac": "5".repeat(64)}));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))
.expect("proof mode");
}
let reference = file_ref(&path);
let mut proof = json!({"case": case, "status": status, "exit": exit, "sequence": sequence, "recipe_sha256": payload["recipe_sha256"], "previous_proof_sha256": previous,
"selection": payload["selection"], "verified_lane_input": null, "commands": [command], "path": reference["path"], "sha256": reference["sha256"], "bytes": reference["bytes"]});
for key in ["review_checkpoint", "terminal_verification"] {
if let Some(value) = payload.get(key) {
proof[key] = value.clone();
}
}
previous = reference["sha256"].as_str().expect("digest").into();
proofs.push(proof);
}
hosts.push(json!({"harness": harness, "status": "passed", "roots": {"project": project, "session": session, "home": home, "cache": cache},
"binary": {"resolution": "path"}, "invocation": {"exit": 0}, "executed_cases": proofs.len(), "artifacts": [file_ref(&artifact)], "machine_proofs": proofs}));
}
write_json(
&self.lifecycle,
&json!({"schema": "shepherd.packed-dogfood-report/1", "candidate_id": fixture.record()["candidate_id"], "version": env!("CARGO_PKG_VERSION"), "status": "complete",
"manifest_path": self.manifest, "manifest_sha256": file_ref(&self.manifest)["sha256"], "source": packages["source"], "matrix_root": matrix,
"applicable_hosts_passed": 3, "applicable_hosts_required": 3, "hosts": hosts}),
);
}
}
#[test]
fn packed_attestation_rechecks_installed_component_compiler_and_frozen_text() {
let fixture = Fixture::new();
let packed = PackedFixture::new(&fixture);
let original = read_json(&packed.manifest);
let run = fixture.root.join(".shepherd/runs/v657/run.json");
let before = fs::read(&run).expect("run bytes");
let installed_component = packed
.installed
.join("@pzzld/component-runtime/runtime/shepherd-component.wasm");
let component_bytes = fs::read(&installed_component).expect("Component bytes");
fs::write(&installed_component, b"mismatched installed Component").expect("mutated Component");
let mut changed = original.clone();
changed["installed"]["sha256"] = json!(tree_hash(&packed.installed));
write_json(&packed.manifest, &changed);
assert!(
!packed.attest(&fixture, "packages").status.success(),
"mismatched installed Component was accepted"
);
fs::write(&installed_component, component_bytes).expect("restore Component");
let generated = packed
.installed
.join("@pzzld/pi-shepherd/.shepherd-generated.json");
let generated_bytes = fs::read(&generated).expect("generated bytes");
let mut generated_value = read_json(&generated);
generated_value["tree_digest"] = json!("0".repeat(64));
write_json(&generated, &generated_value);
changed["installed"]["sha256"] = json!(tree_hash(&packed.installed));
write_json(&packed.manifest, &changed);
assert!(
!packed.attest(&fixture, "packages").status.success(),
"forged compiler manifest was accepted"
);
fs::write(&generated, generated_bytes).expect("restore generated manifest");
for pointer in [
"/candidate_id",
"/candidate/native_cli/sha256",
"/packages/@pzzld~1pi-shepherd/archive/sha256",
] {
changed = original.clone();
*changed
.pointer_mut(pointer)
.expect("package manifest pointer") = json!("0".repeat(64));
write_json(&packed.manifest, &changed);
assert!(
!packed.attest(&fixture, "packages").status.success(),
"forged {pointer} was accepted"
);
}
let prompt = PathBuf::from(
original["candidate"]["dogfood_prompt"]["path"]
.as_str()
.expect("prompt"),
);
let prompt_bytes = fs::read(&prompt).expect("prompt bytes");
fs::write(&prompt, "self-authored acceptance prompt").expect("replace prompt");
changed = original.clone();
changed["candidate"]["dogfood_prompt"] = file_ref(&prompt);
write_json(&packed.manifest, &changed);
assert!(
!packed.attest(&fixture, "packages").status.success(),
"unfrozen prompt was accepted"
);
fs::write(prompt, prompt_bytes).expect("restore prompt");
assert_eq!(
fs::read(run).expect("run bytes"),
before,
"denied unattached input changed candidate state"
);
write_json(&packed.manifest, &original);
let output = packed.attest(&fixture, "packages");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(fixture.verify("packages").status.success());
assert!(
!packed.attest(&fixture, "packages").status.success(),
"package attestation replay succeeded"
);
fs::write(installed_component, "post-attestation mutation").expect("product artifact mutation");
assert!(!fixture.verify("packages").status.success());
assert_eq!(
fixture.record()["state"],
"revoked",
"retained artifact drift was not durably revoked"
);
}
#[test]
fn lifecycle_attestation_rechecks_candidate_hosts_proof_bytes_and_chain() {
let fixture = Fixture::new();
let packed = PackedFixture::new(&fixture);
let output = packed.attest(&fixture, "packages");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
packed.lifecycle(&fixture);
let original = read_json(&packed.lifecycle);
for (pointer, value) in [
("/candidate_id", json!("0".repeat(64))),
("/hosts/0/binary/resolution", json!("override")),
(
"/hosts/0/machine_proofs/0/path",
json!(fixture.base.join("missing-proof.json")),
),
(
"/hosts/0/machine_proofs/1/previous_proof_sha256",
json!("0".repeat(64)),
),
("/hosts/0/machine_proofs/0/selection/run", json!("v656")),
] {
let mut changed = original.clone();
*changed.pointer_mut(pointer).expect("lifecycle pointer") = value;
write_json(&packed.lifecycle, &changed);
assert!(
!packed.attest(&fixture, "lifecycle").status.success(),
"forged lifecycle {pointer} was accepted"
);
assert_eq!(fixture.record()["state"], "packages");
}
write_json(&packed.lifecycle, &original);
let output = packed.attest(&fixture, "lifecycle");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(fixture.record()["state"], "attested");
assert!(fixture.verify("attested").status.success());
assert!(
!packed.attest(&fixture, "lifecycle").status.success(),
"lifecycle replay succeeded"
);
let proof = original["hosts"][0]["machine_proofs"][0]["path"]
.as_str()
.expect("proof path");
fs::write(proof, "proof changed after attestation").expect("mutate proof");
assert!(!fixture.verify("attested").status.success());
assert_eq!(fixture.record()["state"], "revoked");
}
#[test]
fn freeze_binds_native_measurements_and_rejects_refreeze_and_early_pack() {
let fixture = Fixture::new();
let before = fs::read(fixture.root.join(".shepherd/runs/v657/run.json")).expect("run bytes");
fixture.measure();
assert_eq!(
fs::read(fixture.root.join(".shepherd/runs/v657/run.json")).expect("run bytes"),
before,
"manifest is read-only"
);
fixture.freeze();
assert_eq!(fixture.record()["state"], "frozen");
assert!(
!fixture
.invoke(&["candidate", "pack", "--run", "v657"])
.status
.success()
);
assert!(
!fixture
.invoke(&[
"candidate",
"freeze",
"--run",
"v657",
"--source",
&fixture.head(),
"--manifest",
fixture.manifest.to_str().expect("path")
])
.status
.success()
);
assert!(fixture.verify("source").status.success());
fixture.success(&["candidate", "pack", "--run", "v657"]);
assert_eq!(fixture.record()["state"], "packing");
assert!(
!fixture
.invoke(&["candidate", "pack", "--run", "v657"])
.status
.success()
);
}
#[test]
fn changed_product_is_durably_revoked_before_nonzero_exit() {
let fixture = Fixture::new();
fixture.freeze();
assert!(fixture.verify("source").status.success());
fs::write(fixture.root.join("source.txt"), "changed product\n").expect("mutate product");
let output = fixture.verify("source");
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("candidate revoked"));
assert_eq!(fixture.record()["state"], "revoked");
assert!(
fixture.record()["revocation_reason"]
.as_str()
.expect("reason")
.contains("source.txt")
);
assert!(
!fixture.verify("source").status.success(),
"revoked state is terminal"
);
}
#[test]
fn only_current_run_evidence_can_advance_the_frozen_commit() {
let fixture = Fixture::new();
fixture.freeze();
let evidence = fixture.root.join(".shepherd/runs/v657/evidence");
fs::create_dir_all(&evidence).expect("evidence directory");
fs::write(evidence.join("attestation.json"), "{}\n").expect("evidence");
fixture.commit();
let output = fixture.verify("source");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let foreign = fixture.root.join(".shepherd/runs/v656/evidence");
fs::create_dir_all(&foreign).expect("foreign evidence directory");
fs::write(foreign.join("attestation.json"), "{}\n").expect("foreign evidence");
fixture.commit();
assert!(!fixture.verify("source").status.success());
assert_eq!(fixture.record()["state"], "revoked");
}
#[test]
fn freeze_rejects_dirty_source_and_manifest_forgery_without_state_changes() {
let fixture = Fixture::new();
let original = read_json(&fixture.manifest);
let run_path = fixture.root.join(".shepherd/runs/v657/run.json");
let before = fs::read(&run_path).expect("original run");
let mut cases = Vec::new();
for pointer in [
"/compiler/claude",
"/artifacts/native_cli/sha256",
"/artifacts/component_wasm/sha256",
"/files/0/sha256",
] {
let mut changed = original.clone();
*changed.pointer_mut(pointer).expect("manifest field") = json!("0".repeat(64));
cases.push(changed);
}
let mut missing = original.clone();
missing["files"].as_array_mut().expect("files").remove(0);
cases.push(missing);
let mut excluded = original.clone();
excluded["files"][0]["path"] = json!(".shepherd/runs/v657/run.json");
cases.push(excluded);
for changed in cases {
write_json(&fixture.manifest, &changed);
assert!(
!fixture
.invoke(&[
"candidate",
"freeze",
"--run",
"v657",
"--source",
&fixture.head(),
"--manifest",
fixture.manifest.to_str().expect("manifest path")
])
.status
.success()
);
assert_eq!(
fs::read(&run_path).expect("run"),
before,
"denied freeze changed native state"
);
}
write_json(&fixture.manifest, &original);
fs::write(fixture.root.join("new-product.txt"), "uncommitted").expect("dirty file");
assert!(
!fixture
.invoke(&[
"candidate",
"freeze",
"--run",
"v657",
"--source",
&fixture.head(),
"--manifest",
fixture.manifest.to_str().expect("manifest path")
])
.status
.success()
);
assert_eq!(fs::read(&run_path).expect("run"), before);
}
#[test]
fn freeze_rejects_refs_tree_objects_and_self_inclusion() {
let fixture = Fixture::new();
for source in [
"HEAD".to_owned(),
"v6.5.7".to_owned(),
fixture.head()[..7].to_owned(),
"f".repeat(40),
fixture.git(&["rev-parse", "HEAD^{tree}"]),
] {
assert!(
!fixture
.invoke(&[
"candidate",
"freeze",
"--run",
"v657",
"--source",
&source,
"--manifest",
fixture.manifest.to_str().expect("manifest")
])
.status
.success(),
"accepted {source}"
);
}
let inside = fixture.root.join("product-manifest.json");
fs::copy(&fixture.manifest, &inside).expect("self included manifest");
let output = fixture.invoke(&[
"candidate",
"freeze",
"--run",
"v657",
"--source",
&fixture.head(),
"--manifest",
inside.to_str().expect("inside path"),
]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("self-inclusion"));
}
#[test]
fn explicit_revoke_retains_history_and_allows_only_a_fresh_identity() {
let fixture = Fixture::new();
fixture.freeze();
let first = fixture.record()["candidate_id"].clone();
fixture.success(&[
"candidate",
"revoke",
"--run",
"v657",
"--reason",
"measured replacement",
]);
fixture.freeze();
let state = read_json(&fixture.root.join(".shepherd/runs/v657/run.json"));
assert_eq!(
state["candidate_custody"]["history"][0]["candidate_id"],
first
);
assert_ne!(fixture.record()["candidate_id"], first);
}
#[cfg(unix)]
#[test]
fn native_reader_rejects_hardlinks_and_unapproved_symlinks() {
use std::os::unix::fs::symlink;
let fixture = Fixture::new();
let original = read_json(&fixture.manifest);
let alias = fixture.base.join("linked-component.wasm");
fs::hard_link(&fixture.component, &alias).expect("hardlink fixture");
assert!(
!fixture
.invoke(&[
"candidate",
"manifest",
"--run",
"v657",
"--component",
alias.to_str().expect("alias")
])
.status
.success()
);
fs::remove_file(&alias).expect("remove own hardlink");
symlink(&fixture.component, &alias).expect("symlink fixture");
assert!(
!fixture
.invoke(&[
"candidate",
"manifest",
"--run",
"v657",
"--component",
alias.to_str().expect("alias")
])
.status
.success()
);
symlink("source.txt", fixture.root.join("unknown-link")).expect("unknown product link");
fixture.commit();
assert!(
!fixture
.invoke(&[
"candidate",
"manifest",
"--run",
"v657",
"--component",
fixture.component.to_str().expect("component")
])
.status
.success()
);
assert_eq!(read_json(&fixture.manifest), original);
}
#[test]
fn two_native_pack_callers_consume_only_one_locked_reservation() {
use std::process::Stdio;
let fixture = Fixture::new();
fixture.freeze();
assert!(fixture.verify("source").status.success());
let args = ["candidate", "pack", "--run", "v657", "--json"];
let first = fixture
.command(&args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("first pack caller");
let second = fixture
.command(&args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("second pack caller");
let results = [
first.wait_with_output().expect("first result"),
second.wait_with_output().expect("second result"),
];
assert_eq!(
results
.iter()
.filter(|result| result.status.success())
.count(),
1,
"reservation must serialize across processes"
);
assert_eq!(fixture.record()["state"], "packing");
}
#[test]
fn unrelated_commit_with_identical_tree_revokes_native_candidate() {
let fixture = Fixture::new();
fixture.freeze();
let tree = fixture.git(&["rev-parse", "HEAD^{tree}"]);
let unrelated = fixture.git(&["commit-tree", &tree, "-m", "unrelated identical product"]);
fixture.git(&["update-ref", "HEAD", &unrelated]);
let result = fixture.verify("source");
assert!(!result.status.success());
assert_eq!(fixture.record()["state"], "revoked");
}
#[cfg(unix)]
#[test]
fn product_modes_are_measured_even_when_git_ignores_filemode() {
use std::os::unix::fs::PermissionsExt;
let fixture = Fixture::new();
fixture.git(&["config", "core.filemode", "false"]);
fixture.freeze();
fs::set_permissions(
fixture.root.join("source.txt"),
fs::Permissions::from_mode(0o755),
)
.expect("change mode");
assert!(
fixture.git(&["status", "--porcelain"]).is_empty(),
"Git deliberately ignores the mode"
);
assert!(!fixture.verify("source").status.success());
assert_eq!(fixture.record()["state"], "revoked");
}
#[cfg(unix)]
#[test]
fn five_intentional_aliases_bind_link_bytes_and_inventoried_targets() {
use std::os::unix::fs::symlink;
let fixture = Fixture::new();
for path in [
"AGENTS.md",
"agents/worker.md",
"skills/planting/SKILL.md",
"hooks/hooks.json",
"hooks/scripts/entry.sh",
] {
let destination = fixture.root.join(path);
fs::create_dir_all(destination.parent().expect("target parent")).expect("target directory");
fs::write(destination, "canonical alias target fixture\n").expect("target bytes");
}
let aliases = [
("CLAUDE.md", "AGENTS.md"),
("plugins/shepherd/agents", "../../agents"),
(
"plugins/shepherd/hooks/hooks.json",
"../../../hooks/hooks.json",
),
("plugins/shepherd/hooks/scripts", "../../../hooks/scripts"),
("plugins/shepherd/skills", "../../skills"),
];
for (path, target) in aliases {
let destination = fixture.root.join(path);
fs::create_dir_all(destination.parent().expect("link parent")).expect("link directory");
symlink(target, destination).expect("canonical symlink");
}
fixture.commit();
fixture.measure();
let manifest = read_json(&fixture.manifest);
for (path, target) in aliases {
assert!(
manifest["files"]
.as_array()
.expect("files")
.iter()
.any(|file| file["path"] == path
&& file["mode"] == "120000"
&& file["sha256"] == sha256(target.as_bytes()))
);
}
fixture.freeze();
assert!(fixture.verify("source").status.success());
fs::remove_file(fixture.root.join("CLAUDE.md")).expect("remove owned fixture alias");
symlink("source.txt", fixture.root.join("CLAUDE.md")).expect("replace owned fixture alias");
assert!(!fixture.verify("source").status.success());
assert_eq!(fixture.record()["state"], "revoked");
let parent_alias = fixture.base.join("alias-directory");
symlink(&fixture.base, &parent_alias).expect("parent alias");
assert!(
!fixture
.invoke(&[
"candidate",
"manifest",
"--run",
"v657",
"--component",
parent_alias
.join("component.wasm")
.to_str()
.expect("alias path")
])
.status
.success()
);
}
#[cfg(windows)]
#[test]
fn windows_accepts_ordinary_absolute_paths_but_denies_hardlinked_components() {
let fixture = Fixture::new();
let canonical = fixture.component.to_str().expect("Component path");
let ordinary = canonical.strip_prefix(r"\\?\").unwrap_or(canonical);
let result = fixture.invoke(&[
"candidate",
"manifest",
"--run",
"v657",
"--component",
ordinary,
]);
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
let link = fixture.base.join("linked-component.wasm");
fs::hard_link(&fixture.component, &link).expect("hardlinked component");
assert!(
!fixture
.invoke(&[
"candidate",
"manifest",
"--run",
"v657",
"--component",
link.to_str().expect("hardlink path")
])
.status
.success()
);
}