use std::{
collections::BTreeSet,
fs, io,
path::{Component, Path, PathBuf},
process::{Command, Output},
time::{SystemTime, UNIX_EPOCH},
};
use shepherd_cli::shepherd::registry::Registry;
#[cfg(unix)]
use std::io::Read;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
fn binary() -> &'static str {
env!("CARGO_BIN_EXE_shepherd")
}
#[derive(Debug, Eq, PartialEq)]
struct ComponentManifest {
path: PathBuf,
present: bool,
kind: u8,
stat: [u64; 5],
content: Option<Vec<u8>>,
link_target: Option<PathBuf>,
}
fn current_registered_root() -> Result<PathBuf, String> {
let cwd = std::env::current_dir().map_err(|error| format!("current directory: {error}"))?;
let git = |argument: &str| -> Result<PathBuf, String> {
let output = Command::new("git")
.args(["rev-parse", "--path-format=absolute", argument])
.current_dir(&cwd)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE")
.output()
.map_err(|error| format!("git {argument}: {error}"))?;
if !output.status.success() {
return Err(format!("git {argument} failed"));
}
let value = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if value.is_empty() {
return Err(format!("git {argument} returned no path"));
}
let path = PathBuf::from(value);
fs::canonicalize(path).map_err(|error| format!("canonicalize git {argument}: {error}"))
};
let top = git("--show-toplevel")?;
let common = git("--git-common-dir")?;
let git_dir = git("--git-dir")?;
if git_dir == common {
return Ok(top);
}
if common.file_name().is_some_and(|name| name == ".git") {
return common
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| "common git directory has no checkout parent".to_owned());
}
Err("current git worktree has no registered primary checkout".to_owned())
}
fn ensure_isolated_probe_root(root: &Path) -> Result<(), String> {
let root = fs::canonicalize(root).map_err(|error| format!("probe root: {error}"))?;
let primary = current_registered_root()?;
if root == primary || root.starts_with(&primary) {
return Err(format!(
"refusing the primary checkout/current registered root: {}",
root.display()
));
}
let temp = fs::canonicalize(std::env::temp_dir())
.map_err(|error| format!("temporary directory: {error}"))?;
if root == temp || !root.starts_with(&temp) {
return Err(format!(
"probe root is not an isolated temporary root: {}",
root.display()
));
}
Ok(())
}
fn stat_fields(metadata: &fs::Metadata) -> [u64; 5] {
#[cfg(unix)]
{
[
metadata.dev(),
metadata.ino(),
metadata.nlink(),
u64::from(metadata.mode()),
metadata.len(),
]
}
#[cfg(not(unix))]
{
[
0,
0,
0,
u64::from(metadata.permissions().readonly() as u8),
metadata.len(),
]
}
}
fn read_regular_nofollow(path: &Path) -> Vec<u8> {
#[cfg(unix)]
{
use rustix::fs::{Mode, OFlags, open};
let descriptor = open(
path,
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap_or_else(|error| panic!("manifest no-follow open {}: {error}", path.display()));
let mut file = fs::File::from(descriptor);
let mut content = Vec::new();
file.read_to_end(&mut content)
.unwrap_or_else(|error| panic!("manifest content {}: {error}", path.display()));
content
}
#[cfg(not(unix))]
{
fs::read(path)
.unwrap_or_else(|error| panic!("manifest content {}: {error}", path.display()))
}
}
fn component_manifest(paths: &[PathBuf]) -> Vec<ComponentManifest> {
let mut components = BTreeSet::new();
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
std::env::current_dir()
.expect("manifest current directory")
.join(path)
};
let mut current = PathBuf::new();
for component in absolute.components() {
match component {
Component::Prefix(prefix) => current.push(prefix.as_os_str()),
Component::RootDir => current.push(Path::new("/")),
Component::CurDir => {}
Component::ParentDir => {
panic!("manifest path contains traversal: {}", path.display())
}
Component::Normal(name) => current.push(name),
}
if matches!(component, Component::Prefix(_)) {
continue;
}
components.insert(current.clone());
match fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => break,
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => break,
Err(error) => panic!("manifest stat {}: {error}", current.display()),
}
}
}
components
.into_iter()
.map(|path| match fs::symlink_metadata(&path) {
Ok(metadata) => {
let file_type = metadata.file_type();
let kind = if file_type.is_symlink() {
1
} else if file_type.is_dir() {
2
} else if file_type.is_file() {
3
} else {
4
};
let content = file_type.is_file().then(|| read_regular_nofollow(&path));
let link_target = file_type.is_symlink().then(|| {
fs::read_link(&path)
.unwrap_or_else(|error| panic!("manifest link {}: {error}", path.display()))
});
ComponentManifest {
path,
present: true,
kind,
stat: stat_fields(&metadata),
content,
link_target,
}
}
Err(error) if error.kind() == io::ErrorKind::NotFound => ComponentManifest {
path,
present: false,
kind: 0,
stat: [0; 5],
content: None,
link_target: None,
},
Err(error) => panic!("manifest stat: {error}"),
})
.collect()
}
fn candidate_paths(root: &Path, args: &[&str]) -> Vec<PathBuf> {
let mut paths = vec![
root.to_path_buf(),
root.join(".shepherd"),
root.join(".shepherd/shepherd.db"),
root.join("home"),
root.join("home/.shepherd"),
];
for argument in args.iter().skip(2) {
if argument.starts_with('-') {
continue;
}
let path = PathBuf::from(argument);
paths.push(if path.is_absolute() {
path
} else {
root.join(path)
});
}
paths
}
fn stable_probe_manifest(
manifest: Vec<ComponentManifest>,
candidates: &[PathBuf],
) -> Vec<ComponentManifest> {
manifest
.into_iter()
.filter(|entry| {
candidates
.iter()
.any(|candidate| entry.path == *candidate || entry.path.starts_with(candidate))
})
.collect()
}
fn ensure_explicit_registry(root: &Path) {
ensure_isolated_probe_root(root).expect("native probe root must be isolated");
let registry = root.join(".shepherd/shepherd.db");
let metadata = fs::symlink_metadata(®istry).expect("isolated native registry");
assert!(
metadata.file_type().is_file(),
"registry must be a regular file"
);
assert!(
!metadata.file_type().is_symlink(),
"registry must not be a symlink"
);
assert_eq!(
fs::canonicalize(®istry).expect("canonical registry"),
registry,
"registry must stay below the explicit project root"
);
}
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-seed-{label}-{nonce:x}"));
fs::create_dir_all(&root).expect("fixture");
let root = fs::canonicalize(root).expect("canonical fixture");
ensure_isolated_probe_root(&root).expect("fixture must not be the primary root");
let git = Command::new("git")
.args(["init", "--quiet"])
.current_dir(&root)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE")
.status()
.expect("initialize isolated fixture repository");
assert!(git.success(), "git init must succeed");
fs::create_dir_all(root.join(".shepherd")).expect("fixture namespace");
fs::create_dir_all(root.join("home")).expect("fixture home");
let registry = root.join(".shepherd/shepherd.db");
let registry_handle = Registry::open_migrated(®istry).expect("isolated fixture registry");
drop(registry_handle);
ensure_explicit_registry(&root);
root
}
fn write(root: &Path, name: &str, content: &str) -> PathBuf {
let path = root.join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("fixture parent");
}
fs::write(&path, content).expect("fixture file");
path
}
fn run(root: &Path, args: &[&str]) -> Output {
ensure_explicit_registry(root);
let candidates = candidate_paths(root, args);
let before = stable_probe_manifest(component_manifest(&candidates), &candidates);
let output = Command::new(binary())
.args(args)
.current_dir(root)
.env("HOME", root.join("home"))
.env("SHEPHERD_HOME", root.join("home/.shepherd"))
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE")
.env_remove("SHEPHERD_WORKDIR")
.env_remove("SHCTX_ROOT_OVERRIDE")
.env_remove("SHCTX_DB")
.output()
.expect("native seed command");
let after = stable_probe_manifest(component_manifest(&candidates), &candidates);
if before != after {
let changed = before
.iter()
.zip(&after)
.filter(|(left, right)| left != right)
.map(|(left, right)| format!("{}: {left:?} -> {right:?}", left.path.display()))
.collect::<Vec<_>>();
panic!("native seed probe changed candidate paths: {changed:?}");
}
output
}
fn fixture_paths(root: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
fn visit(path: &Path, paths: &mut Vec<PathBuf>) {
paths.push(path.to_path_buf());
let Ok(metadata) = fs::symlink_metadata(path) else {
return;
};
if !metadata.file_type().is_dir() || metadata.file_type().is_symlink() {
return;
}
for entry in fs::read_dir(path).expect("list fixture path") {
visit(&entry.expect("fixture directory entry").path(), paths);
}
}
visit(root, &mut paths);
paths
}
fn cleanup(root: &Path) {
ensure_isolated_probe_root(root).expect("cleanup root must remain isolated");
let newly_created = fixture_paths(root);
assert!(
!newly_created.is_empty(),
"cleanup allow-list must include the root"
);
for path in &newly_created {
assert!(
path == root || path.starts_with(root),
"cleanup escaped the newly-created fixture: {}",
path.display()
);
}
let mut removal_order = newly_created;
removal_order.sort_by_key(|path| std::cmp::Reverse(path.components().count()));
for path in removal_order {
let Ok(metadata) = fs::symlink_metadata(&path) else {
continue;
};
if metadata.file_type().is_dir() && !metadata.file_type().is_symlink() {
fs::remove_dir(&path).unwrap_or_else(|error| {
panic!(
"cleanup touched an unlisted or non-empty path {}: {error}",
path.display()
)
});
} else {
fs::remove_file(&path).unwrap_or_else(|error| {
panic!(
"cleanup failed for newly-created path {}: {error}",
path.display()
)
});
}
}
assert!(!root.exists(), "cleanup left the isolated root behind");
}
fn contract_fixture(root: &Path, name: &str) -> PathBuf {
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/seed-contract")
.join(name);
let run_dir = root.join(".shepherd/runs/v999");
fs::create_dir_all(&run_dir).expect("contract run directory");
fs::copy(source.join("mesh.md"), run_dir.join("mesh.md")).expect("contract mesh");
fs::copy(source.join("seed.md"), run_dir.join("seed.md")).expect("contract seed");
write(
root,
".shepherd/runs/v999/run.json",
r#"{"schema_version":1,"run":"v999","kind":"sprint","status":"planted","seed":""}"#,
);
run_dir.join("seed.md")
}
#[test]
fn typed_seed_contract_accepts_the_complete_positive_fixture() {
let root = fixture("typed-positive");
let seed = contract_fixture(&root, "valid");
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(
output.status.code(),
Some(0),
"stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("OK:"));
assert!(output.stderr.is_empty());
cleanup(&root);
}
#[test]
fn typed_seed_contract_rejects_each_structural_negative_fixture() {
let root = fixture("typed-negative");
for (name, expected) in [
("missing-schema", "schema"),
("duplicate-id", "duplicate"),
("unresolved-source", "mesh source"),
("placeholder", "placeholder"),
("blocking-decision", "blocking"),
] {
let seed = contract_fixture(&root, name);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(
output.status.code(),
Some(1),
"{name}: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
String::from_utf8_lossy(&output.stdout)
.to_ascii_lowercase()
.contains(expected),
"{name}: stdout={}",
String::from_utf8_lossy(&output.stdout)
);
}
cleanup(&root);
}
#[test]
fn prose_only_seed_is_rejected_without_a_typed_contract() {
let root = fixture("prose-only");
let run_dir = root.join(".shepherd/runs/v999");
fs::create_dir_all(&run_dir).expect("run directory");
write(&run_dir, "mesh.md", "# mesh\n");
write(
&run_dir,
"run.json",
r#"{"schema_version":1,"run":"v999","kind":"sprint","status":"planted","seed":""}"#,
);
let seed = write(&run_dir, "seed.md", "# prose seed\noperator intent\n");
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stdout).contains("typed seed contract"));
cleanup(&root);
}
#[test]
fn typed_seed_rejects_a_non_planted_authoritative_run() {
let root = fixture("typed-wrong-state");
let seed = contract_fixture(&root, "valid");
write(
&root,
".shepherd/runs/v999/run.json",
r#"{"schema_version":1,"run":"v999","kind":"sprint","status":"executing","seed":""}"#,
);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stdout).contains("not planted"));
cleanup(&root);
}
#[test]
fn content_preflight_reads_temp_content_against_the_canonical_target() {
let root = fixture("content-preflight");
let seed = contract_fixture(&root, "valid");
let content = root.join("temporary-write-content");
fs::copy(&seed, &content).expect("temporary content");
let output = run(
&root,
&[
"seed",
"verify-content",
&content.to_string_lossy(),
&seed.to_string_lossy(),
],
);
assert_eq!(
output.status.code(),
Some(0),
"stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
cleanup(&root);
}
#[test]
fn content_preflight_does_not_relax_a_missing_scope_for_a_closed_target() {
let root = fixture("content-preflight-closed");
let seed = contract_fixture(&root, "valid");
write(&root, ".shepherd/runs/v999/close.md", "closed\n");
let content = root.join("temporary-write-content");
let mut payload = fs::read_to_string(&seed).expect("seed bytes");
payload.push_str("\nfile_scope:\n - missing-output.rs\n");
fs::write(&content, payload).expect("temporary content");
let output = run(
&root,
&[
"seed",
"verify-content",
&content.to_string_lossy(),
&seed.to_string_lossy(),
],
);
assert_eq!(output.status.code(), Some(1));
assert!(
String::from_utf8_lossy(&output.stdout).contains(
"file_scope path does not resolve and is not marked (NEW): missing-output.rs"
)
);
cleanup(&root);
}
#[cfg(unix)]
#[test]
fn typed_seed_rejects_a_fifo_source_before_opening_it() {
use std::os::unix::fs::FileTypeExt;
let root = fixture("typed-fifo-source");
let seed = contract_fixture(&root, "valid");
let fifo = root.join("fifo-source.txt");
let status = std::process::Command::new("mkfifo")
.arg(&fifo)
.status()
.expect("mkfifo command");
assert!(status.success(), "mkfifo failed: {status}");
assert!(
fs::symlink_metadata(&fifo)
.expect("fifo metadata")
.file_type()
.is_fifo()
);
let content = fs::read_to_string(&seed).expect("seed bytes").replace(
"sources: [MESH-1, MESH-2]",
"sources: [MESH-1, fifo-source.txt]",
);
fs::write(&seed, content).expect("mutate source list");
let mesh = root.join(".shepherd/runs/v999/mesh.md");
let mut mesh_content = fs::read_to_string(&mesh).expect("mesh bytes");
mesh_content
.push_str("\n## fifo-source.txt\nA FIFO must never be opened by seed verification.\n");
fs::write(mesh, mesh_content).expect("mutate mesh");
ensure_explicit_registry(&root);
let seed_argument = seed.to_string_lossy();
let args = ["seed", "verify", seed_argument.as_ref()];
let candidates = candidate_paths(&root, &args);
let before = stable_probe_manifest(component_manifest(&candidates), &candidates);
let mut child = Command::new(binary())
.args(args)
.current_dir(&root)
.env("HOME", root.join("home"))
.env("SHEPHERD_HOME", root.join("home/.shepherd"))
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE")
.env_remove("SHEPHERD_WORKDIR")
.env_remove("SHCTX_ROOT_OVERRIDE")
.env_remove("SHCTX_DB")
.spawn()
.expect("native seed command");
let mut finished = None;
for _ in 0..20 {
if let Some(status) = child.try_wait().expect("poll seed command") {
finished = Some(status);
break;
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
let status = finished.unwrap_or_else(|| {
child.kill().expect("kill blocked seed command");
child.wait().expect("reap blocked seed command")
});
assert_eq!(
status.code(),
Some(1),
"FIFO verification blocked or returned wrong status"
);
let after = stable_probe_manifest(component_manifest(&candidates), &candidates);
assert_eq!(
before, after,
"FIFO seed probe changed an isolated candidate"
);
cleanup(&root);
}
#[cfg(unix)]
#[test]
fn typed_seed_rejects_a_symlinked_scope_parent_before_a_missing_leaf() {
use std::os::unix::fs::symlink;
let root = fixture("typed-scope-symlink");
let seed = contract_fixture(&root, "valid");
write(&root, "outside/marker.txt", "outside\n");
symlink(root.join("outside"), root.join("link")).expect("scope symlink");
let content = fs::read_to_string(&seed)
.expect("seed bytes")
.replace("include: [src, tests]", "include: [link/new.rs]");
fs::write(&seed, content).expect("mutate scope");
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("scope.include") && stdout.contains("symlink"),
"stdout={stdout}"
);
cleanup(&root);
}
#[test]
fn typed_seed_rejects_a_postcondition_that_only_contains_unplanted() {
let root = fixture("typed-unplanted-word");
let seed = contract_fixture(&root, "valid");
let content = fs::read_to_string(&seed)
.expect("seed bytes")
.replace("run remains planted", "run is unplanted");
fs::write(&seed, content).expect("mutate postcondition");
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stdout).contains("remain planted"));
cleanup(&root);
}
#[test]
fn typed_seed_rejects_a_seed_from_another_project_root() {
let root = fixture("typed-project-boundary");
let outside = fixture("typed-project-boundary-outside");
let seed = contract_fixture(&outside, "valid");
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stdout).contains("project"));
cleanup(&root);
cleanup(&outside);
}
#[test]
fn usage_and_validation_have_stable_streams_and_exit_codes() {
let root = fixture("usage");
let bare = run(&root, &["seed"]);
assert!(bare.status.success());
assert!(String::from_utf8_lossy(&bare.stdout).starts_with("shepherd seed verify"));
assert!(bare.stderr.is_empty());
let help = run(&root, &["seed", "--help"]);
assert_eq!(help.stdout, bare.stdout);
assert!(help.status.success());
let unknown = run(&root, &["seed", "bogus"]);
assert_eq!(unknown.status.code(), Some(2));
assert!(unknown.stdout.is_empty());
assert!(String::from_utf8_lossy(&unknown.stderr).starts_with("unknown subcommand: bogus\n"));
let missing = run(&root, &["seed", "verify"]);
assert_eq!(missing.status.code(), Some(2));
assert_eq!(missing.stderr, b"ERR: seed verify needs a <path>\n");
let flag = run(&root, &["seed", "verify", "--bogus"]);
assert_eq!(flag.status.code(), Some(2));
assert_eq!(flag.stderr, b"unknown flag: --bogus\n");
cleanup(&root);
}
#[test]
fn universal_hard_checks_preserve_order_and_quiet_only_suppresses_output() {
let root = fixture("hard");
let body = format!(
"kind: patch-seed\nTODO: resolve\nLane 4 is prescriptive\n{}\n",
(0..402)
.map(|index| format!("line {index}"))
.collect::<Vec<_>>()
.join("\n")
);
let path = write(&root, "broken.seed.md", &body);
let shown = path.to_string_lossy();
let loud = run(&root, &["seed", "verify", &shown]);
assert_eq!(loud.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&loud.stdout);
let footprint = stdout.find("HARD footprint").expect("footprint");
let todo = stdout.find("HARD TODO:/FIXME:").expect("todo");
let lane = stdout.find("HARD prescriptive 'Lane N'").expect("lane");
assert!(footprint < todo && todo < lane);
assert!(stdout.ends_with("FAIL: 4 hard failure(s), 0 warning(s)\n"));
assert!(loud.stderr.is_empty());
let quiet = run(&root, &["seed", "verify", &shown, "--quiet"]);
assert_eq!(quiet.status.code(), Some(1));
assert!(quiet.stdout.is_empty());
assert!(quiet.stderr.is_empty());
cleanup(&root);
}
#[test]
fn file_scope_literals_new_markers_flow_lists_and_globs_are_deterministic() {
let root = fixture("scope");
let existing = write(&root, "src/exists.rs", "// fixture\n");
let missing = root.join("src/missing.rs");
let content = format!(
"kind: sprint-seed\nmilestone: v1\nfile_scope:\n exclusive: [{}, {}]\n - {} (NEW - planned)\n - {}\n---\n",
existing.display(),
missing.display(),
root.join("src/new.rs").display(),
root.join("src/*.rs").display(),
);
let seed = write(&root, "scope.seed.md", &content);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains(&format!(
"file_scope path does not resolve and is not marked (NEW): {}",
missing.display()
)));
assert_eq!(
stdout.matches("file_scope path does not resolve").count(),
1
);
cleanup(&root);
}
#[test]
fn deliverable_and_canonical_warning_rules_match_the_frozen_policy() {
let root = fixture("canonical");
let seed = write(
&root,
"canonical.seed.md",
"kind: sprint-seed\n### Medium delivery [MEDIUM]\n**Priority:** MEDIUM\nPhase 0 mesh\n| 1 | first |\n| 2 | second |\n",
);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("1 deliverable block(s) carry a priority but no **GH:** anchor"));
assert!(stdout.contains("Phase 0 mesh has 2 row(s) (< 8 recommended)"));
assert!(stdout.contains("no deliverable ranked CRITICAL or HIGH"));
assert!(stdout.contains("frontmatter missing 'milestone:'"));
assert!(stdout.ends_with("FAIL: 2 hard failure(s), 3 warning(s)\n"));
cleanup(&root);
}
#[test]
fn clean_seed_and_trailing_blank_lines_are_stable() {
let root = fixture("clean");
let seed = write(
&root,
"clean.seed.md",
"kind: patch-seed\nordinary prose\n\n\n",
);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stdout).contains("typed seed contract"));
assert!(output.stderr.is_empty());
cleanup(&root);
}
#[test]
fn crlf_input_has_stable_verdict_bytes() {
let root = fixture("crlf");
let seed = write(
&root,
"crlf.seed.md",
"kind: sprint-seed\r\nordinary prose\r\n",
);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stdout).contains("typed seed contract"));
assert!(output.stderr.is_empty());
cleanup(&root);
}
#[cfg(unix)]
#[test]
fn shell_style_globs_preserve_ranges_parent_components_and_dangling_links() {
use std::os::unix::fs::symlink;
let root = fixture("shell-globs");
let parent = root.parent().expect("fixture parent");
let sibling_name = format!(
"{}-shared",
root.file_name().expect("fixture name").to_string_lossy()
);
let sibling = parent.join(&sibling_name);
fs::create_dir_all(&sibling).expect("sibling fixture");
write(&sibling, "b.rs", "// bracket range fixture\n");
fs::create_dir_all(root.join("links")).expect("links fixture");
symlink(root.join("missing-target"), root.join("links/dangling.rs"))
.expect("dangling symlink fixture");
let seed = write(
&root,
"glob.seed.md",
&format!(
"kind: sprint-seed\nmilestone: v1\nfile_scope:\n - ../{sibling_name}/[a-z].rs\n - links/dangling.*\n---\n"
),
);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stdout).contains("typed seed contract"));
assert!(output.stderr.is_empty());
cleanup(&root);
cleanup(&sibling);
}
#[test]
fn closed_run_sibling_close_md_downgrades_only_the_scope_resolution_hard_failure() {
let root = fixture("closed-scope");
let missing = root.join("bin");
let content = format!(
"kind: patch-seed\nmilestone: v1\nfile_scope:\n - {}\n---\n",
missing.display()
);
let seed = write(&root, "runs/v999/seed.md", &content);
let live = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(live.status.code(), Some(1));
let live_stdout = String::from_utf8_lossy(&live.stdout);
assert!(
live_stdout.contains(&format!(
"HARD file_scope path does not resolve and is not marked (NEW): {}",
missing.display()
)),
"stdout={live_stdout}"
);
assert!(live_stdout.ends_with("FAIL: 2 hard failure(s), 0 warning(s)\n"));
assert!(live.stderr.is_empty());
write(&root, "runs/v999/close.md", "closed\n");
let closed = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(closed.status.code(), Some(1));
let closed_stdout = String::from_utf8_lossy(&closed.stdout);
assert!(
closed_stdout.contains(&format!(
"warn file_scope path does not resolve: {} (run closed — close.md present; a closed run's seed is a record, not a proposal)",
missing.display()
)),
"stdout={closed_stdout}"
);
assert!(closed_stdout.ends_with("FAIL: 1 hard failure(s), 1 warning(s)\n"));
assert!(closed.stderr.is_empty());
cleanup(&root);
}
fn seed_with_footprint(kind: &str, total_lines: usize) -> String {
let filler = (0..total_lines - 1)
.map(|index| format!("line {index}"))
.collect::<Vec<_>>()
.join("\n");
format!("kind: {kind}\n{filler}\n")
}
#[test]
fn live_sprint_seed_over_the_ceiling_is_still_hard() {
let root = fixture("ceiling-sprint");
let content = seed_with_footprint("sprint-seed", 401);
let seed = write(&root, "over.seed.md", &content);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("HARD footprint 401 lines > cap 400 (kind=sprint-seed)"),
"stdout={stdout}"
);
cleanup(&root);
}
#[test]
fn live_patch_seed_over_the_ceiling_cannot_relabel_its_way_out() {
let root = fixture("ceiling-patch");
let content = seed_with_footprint("patch-seed", 401);
let seed = write(&root, "over.seed.md", &content);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("HARD footprint 401 lines > cap 400 (kind=patch-seed)"),
"stdout={stdout}"
);
cleanup(&root);
}
#[test]
fn live_patch_seed_under_the_ceiling_still_reports_the_mislabel_warn() {
let root = fixture("mislabel-patch");
let content = seed_with_footprint("patch-seed", 250);
let seed = write(&root, "sprint-shaped.seed.md", &content);
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("typed seed contract"));
assert!(
stdout.contains(
"warn footprint 250 lines > patch cap 200 (kind=patch-seed) — sprint-shaped; relabel or move evidence to mesh.md"
),
"stdout={stdout}"
);
cleanup(&root);
}
#[test]
fn todo_marker_stays_hard_even_under_a_closed_run() {
let root = fixture("closed-todo");
let content = "kind: patch-seed\nTODO: resolve before commit\n";
let seed = write(&root, "runs/v998/seed.md", content);
write(&root, "runs/v998/close.md", "closed\n");
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("HARD TODO:/FIXME: marker(s) present — resolve before commit"),
"closed-run relaxation leaked past the file_scope site: stdout={stdout}"
);
cleanup(&root);
}
#[test]
fn close_md_beside_a_non_run_shaped_seed_path_does_not_relax_anything() {
let root = fixture("non-run-shaped");
let missing = root.join("bin");
let content = format!(
"kind: patch-seed\nmilestone: v1\nfile_scope:\n - {}\n---\n",
missing.display()
);
let seed = write(&root, "shep-seed.ABC123", &content);
write(&root, "close.md", "closed\n");
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(&format!(
"HARD file_scope path does not resolve and is not marked (NEW): {}",
missing.display()
)),
"stdout={stdout}"
);
cleanup(&root);
}
#[test]
fn seed_input_is_bounded_before_allocating_the_whole_file() {
let root = fixture("size-bound");
let seed = root.join("oversized.seed.md");
fs::write(&seed, vec![b'x'; 1_048_577]).expect("oversized fixture");
let output = run(&root, &["seed", "verify", &seed.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1));
assert!(output.stdout.is_empty());
assert_eq!(
String::from_utf8_lossy(&output.stderr),
format!(
"ERROR: seed input exceeds 1048576 bytes: {}\n",
seed.display()
)
);
cleanup(&root);
}
#[test]
fn native_seed_probe_rejects_the_current_registered_root() {
let primary = current_registered_root().expect("current registered root");
let error = ensure_isolated_probe_root(&primary).expect_err("primary root must be refused");
assert!(error.contains("primary checkout"), "error={error}");
}