use std::{
fs,
path::{Path, PathBuf},
process::{Command, Output},
time::{SystemTime, UNIX_EPOCH},
};
fn binary() -> &'static str {
env!("CARGO_BIN_EXE_shepherd")
}
fn fixture(label: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos();
let root = std::env::temp_dir().join(format!(
"shepherd-compile-{label}-{}-{nonce:x}",
std::process::id()
));
fs::create_dir_all(&root).expect("fixture");
root
}
fn run(cwd: &Path, args: &[&str]) -> Output {
Command::new(binary())
.args(args)
.current_dir(cwd)
.output()
.expect("run shepherd")
}
fn copy_tree(source: &Path, destination: &Path) {
fs::create_dir_all(destination).expect("copy destination");
for entry in fs::read_dir(source).expect("copy source") {
let entry = entry.expect("copy entry");
let target = destination.join(entry.file_name());
if entry.file_type().expect("copy type").is_dir() {
copy_tree(&entry.path(), &target);
} else {
fs::copy(entry.path(), target).expect("copy file");
}
}
}
fn write_startup_fixture(root: &Path) -> PathBuf {
let content = root.join("content");
fs::create_dir_all(content.join("roles")).expect("roles directory");
fs::create_dir_all(content.join("skills/example/scripts")).expect("skill scripts directory");
fs::write(
content.join("roles/example.md"),
"---\nrole: example\ndescription: \"Example role\"\nsource: agents/example.md\nmodel_hint: standard\nwrite_eligible: true\ndispatchable: true\ncapabilities: [read]\nskill: example\nwrite_scope: \"assigned scope\"\n---\n\n# example\n\nDo the work.\n",
)
.expect("example role");
fs::write(
content.join("skills/example/SKILL.md"),
"---\nname: example\ndescription: \"Example startup skill\"\nsource: skills/example/SKILL.md\nportability: cross-harness\n---\n\n# example\n\nFollow the contract.\n",
)
.expect("example skill");
fs::write(
content.join("skills/example/scripts/check.sh"),
"#!/bin/sh\nexit 0\n",
)
.expect("example script");
content
}
#[test]
fn standalone_binary_compiles_embedded_content_without_a_checkout() {
let root = fixture("embedded");
let copied = root.join("shepherd");
fs::copy(binary(), &copied).expect("copy binary");
let output = Command::new(&copied)
.args(["compile", "--target", "claude"])
.current_dir(&root)
.output()
.expect("run copied binary");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let manifest: serde_json::Value = serde_json::from_slice(&output.stdout).expect("manifest");
assert_eq!(manifest["schema"], "shepherd.compiled-tree/4");
assert_eq!(manifest["target"], "claude");
assert_eq!(manifest["files"].as_array().expect("files").len(), 38);
assert_eq!(manifest["roles"].as_array().expect("roles").len(), 9);
let coder = manifest["roles"]
.as_array()
.expect("roles")
.iter()
.find(|role| role["role"] == "coder")
.expect("coder role contract");
assert_eq!(coder["carrier_path"], "agents/coder.md");
assert_eq!(coder["model_hint"], "standard");
assert_eq!(coder["model"], "sonnet");
assert!(coder["profile"].is_null());
assert!(
coder["tools"]
.as_array()
.expect("tools")
.iter()
.any(|tool| tool == "Write")
);
assert!(output.stderr.is_empty());
fs::remove_dir_all(root).expect("cleanup");
}
#[cfg(unix)]
#[test]
fn manifest_v4_materializes_and_verifies_startup_skills_and_file_modes() {
let root = fixture("startup-modes");
let content = write_startup_fixture(&root);
let output_root = root.join("pi");
let output = run(
&root,
&[
"compile",
"--target",
"pi",
"--content-dir",
content.to_str().expect("content path"),
"--out",
output_root.to_str().expect("output path"),
],
);
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let manifest_path = output_root.join(".shepherd-generated.json");
let manifest_bytes = fs::read(&manifest_path).expect("generated manifest");
let manifest: serde_json::Value =
serde_json::from_slice(&manifest_bytes).expect("manifest JSON");
assert_eq!(manifest["schema"], "shepherd.compiled-tree/4");
let role = &manifest["roles"][0];
assert_eq!(role["startup_skill"], "example");
assert_eq!(
role["startup_skill_sha256"]
.as_str()
.expect("startup digest")
.len(),
64
);
let script = manifest["files"]
.as_array()
.expect("manifest files")
.iter()
.find(|file| file["path"] == "skills/example/scripts/check.sh")
.expect("script entry");
assert_eq!(script["mode"], 0o755);
let generated_script = output_root.join("skills/example/scripts/check.sh");
{
use std::os::unix::fs::PermissionsExt as _;
let mut legacy = manifest.clone();
legacy["schema"] = serde_json::Value::String("shepherd.compiled-tree/3".into());
for role in legacy["roles"].as_array_mut().expect("legacy roles") {
role.as_object_mut()
.expect("legacy role")
.remove("startup_skill");
role.as_object_mut()
.expect("legacy role")
.remove("startup_skill_sha256");
}
for file in legacy["files"].as_array_mut().expect("legacy files") {
file.as_object_mut().expect("legacy file").remove("mode");
}
let mut legacy_bytes = serde_json::to_vec_pretty(&legacy).expect("legacy manifest");
legacy_bytes.push(b'\n');
fs::write(&manifest_path, legacy_bytes).expect("legacy manifest bytes");
let mut permissions = fs::metadata(&generated_script)
.expect("script metadata")
.permissions();
permissions.set_mode(0o644);
fs::set_permissions(&generated_script, permissions).expect("legacy script mode");
let upgraded = run(
&root,
&[
"compile",
"--target",
"pi",
"--content-dir",
content.to_str().expect("content path"),
"--out",
output_root.to_str().expect("output path"),
],
);
assert!(
upgraded.status.success(),
"stderr={}",
String::from_utf8_lossy(&upgraded.stderr)
);
assert_eq!(
fs::read(&manifest_path).expect("upgraded manifest"),
manifest_bytes
);
assert_eq!(
fs::metadata(&generated_script)
.expect("script metadata")
.permissions()
.mode()
& 0o777,
0o755
);
let mut permissions = fs::metadata(&generated_script)
.expect("script metadata")
.permissions();
permissions.set_mode(0o4755);
fs::set_permissions(&generated_script, permissions).expect("drift script mode");
let drift = run(
&root,
&[
"compile",
"--target",
"pi",
"--content-dir",
content.to_str().expect("content path"),
"--out",
output_root.to_str().expect("output path"),
"--check",
],
);
assert_eq!(drift.status.code(), Some(1));
assert!(String::from_utf8_lossy(&drift.stderr).contains("mode"));
let mut permissions = fs::metadata(&generated_script)
.expect("drifted script metadata")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(&generated_script, permissions).expect("restore script mode");
}
fs::remove_dir_all(content.join("skills/example")).expect("remove fixture skill");
let missing = run(
&root,
&[
"compile",
"--target",
"pi",
"--content-dir",
content.to_str().expect("content path"),
"--out",
output_root.to_str().expect("output path"),
"--check",
],
);
assert_eq!(missing.status.code(), Some(1));
assert!(String::from_utf8_lossy(&missing.stderr).contains("zero skill"));
assert_eq!(
fs::read(&manifest_path).expect("preserved manifest"),
manifest_bytes
);
fs::remove_dir_all(root).expect("cleanup");
}
#[cfg(not(unix))]
#[test]
fn executable_generated_files_fail_closed_without_posix_modes() {
let root = fixture("non-posix-modes");
let content = write_startup_fixture(&root);
let output_root = root.join("pi");
let output = run(
&root,
&[
"compile",
"--target",
"pi",
"--content-dir",
content.to_str().expect("content path"),
"--out",
output_root.to_str().expect("output path"),
],
);
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stderr).contains("without POSIX modes"));
assert!(!output_root.exists());
fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn materialize_is_reproducible_and_check_is_read_only() {
let root = fixture("materialize");
let output_root = root.join("codex");
let output = run(
&root,
&[
"compile",
"--target",
"codex",
"--out",
output_root.to_str().expect("output path"),
],
);
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stdout.is_empty());
assert!(output_root.join(".shepherd-generated.json").is_file());
assert!(output_root.join("shepherd.codex.toml").is_file());
let check = run(
&root,
&[
"compile",
"--target",
"codex",
"--out",
output_root.to_str().expect("output path"),
"--check",
],
);
assert!(
check.status.success(),
"stderr={}",
String::from_utf8_lossy(&check.stderr)
);
assert!(check.stdout.is_empty());
assert!(check.stderr.is_empty());
let generated = output_root.join(".agents/skills/thinking/SKILL.md");
let original = fs::read(&generated).expect("generated skill");
fs::write(&generated, b"hand edited\n").expect("drift");
let drift = run(
&root,
&[
"compile",
"--target",
"codex",
"--out",
output_root.to_str().expect("output path"),
"--check",
],
);
assert_eq!(drift.status.code(), Some(1));
assert!(drift.stdout.is_empty());
assert!(String::from_utf8_lossy(&drift.stderr).contains("generated file drift"));
assert_eq!(
fs::read(&generated).expect("preserved drift"),
b"hand edited\n"
);
let overwrite = run(
&root,
&[
"compile",
"--target",
"codex",
"--out",
output_root.to_str().expect("output path"),
],
);
assert_eq!(overwrite.status.code(), Some(1));
assert_eq!(
fs::read(&generated).expect("still preserved"),
b"hand edited\n"
);
fs::write(&generated, original).expect("restore generated file");
let replay = run(
&root,
&[
"compile",
"--target",
"codex",
"--out",
output_root.to_str().expect("output path"),
],
);
assert!(replay.status.success());
fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn materialize_refuses_unowned_targets_and_symlink_roots() {
let root = fixture("ownership");
let output_root = root.join("unowned");
fs::create_dir_all(&output_root).expect("unowned root");
fs::write(output_root.join("shepherd.codex.toml"), b"user file\n").expect("user file");
let unowned = run(
&root,
&[
"compile",
"--target",
"codex",
"--out",
output_root.to_str().expect("output path"),
],
);
assert_eq!(unowned.status.code(), Some(1));
assert!(String::from_utf8_lossy(&unowned.stderr).contains("not owned"));
assert_eq!(
fs::read(output_root.join("shepherd.codex.toml")).expect("user file"),
b"user file\n"
);
#[cfg(unix)]
{
let real = root.join("real");
fs::create_dir_all(&real).expect("real root");
let linked = root.join("linked");
std::os::unix::fs::symlink(&real, &linked).expect("symlink root");
let linked_output = run(
&root,
&[
"compile",
"--target",
"codex",
"--out",
linked.to_str().expect("linked path"),
],
);
assert_eq!(linked_output.status.code(), Some(1));
assert!(real.read_dir().expect("real directory").next().is_none());
}
fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn stale_generated_files_are_removed_only_with_intact_prior_provenance() {
let root = fixture("stale");
let authored = root.join("content");
copy_tree(
&PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../content"),
&authored,
);
let output_root = root.join("claude");
let common = [
"compile",
"--target",
"claude",
"--out",
output_root.to_str().expect("output path"),
"--content-dir",
authored.to_str().expect("content path"),
];
let initial = run(&root, &common);
assert!(
initial.status.success(),
"stderr={}",
String::from_utf8_lossy(&initial.stderr)
);
let stale = output_root.join("skills/adaptation/SKILL.md");
assert!(stale.is_file());
fs::remove_dir_all(authored.join("skills/adaptation")).expect("remove authored skill");
fs::write(&stale, b"operator change\n").expect("drift stale file");
let refused = run(&root, &common);
assert_eq!(refused.status.code(), Some(1));
assert_eq!(
fs::read(&stale).expect("preserved stale drift"),
b"operator change\n"
);
let original = embedded_skill("adaptation");
fs::write(&stale, original).expect("restore generated stale file");
let updated = run(&root, &common);
assert!(
updated.status.success(),
"stderr={}",
String::from_utf8_lossy(&updated.stderr)
);
assert!(!stale.exists());
assert!(!output_root.join("skills/adaptation").exists());
let manifest: serde_json::Value = serde_json::from_slice(
&fs::read(output_root.join(".shepherd-generated.json")).expect("manifest"),
)
.expect("manifest JSON");
assert_eq!(manifest["files"].as_array().expect("files").len(), 37);
fs::remove_dir_all(root).expect("cleanup");
}
fn embedded_skill(name: &str) -> Vec<u8> {
let root = fixture("expected-skill");
let output = root.join("claude");
let result = run(
&root,
&[
"compile",
"--target",
"claude",
"--out",
output.to_str().expect("output path"),
],
);
assert!(result.status.success());
let bytes = fs::read(output.join(format!("skills/{name}/SKILL.md"))).expect("embedded skill");
fs::remove_dir_all(root).expect("cleanup expected skill");
bytes
}
#[test]
fn removed_legacy_commands_never_fall_through_to_another_cli() {
let root = fixture("retired-command");
let output = run(&root, &["style", "show"]);
assert_eq!(output.status.code(), Some(1));
assert!(output.stdout.is_empty());
assert_eq!(
String::from_utf8(output.stderr).expect("UTF-8 stderr"),
"ERROR: command `style` is unavailable in the canonical Rust CLI; legacy Python, Bash, and Node command authorities are retired\n"
);
fs::remove_dir_all(root).expect("cleanup");
}