use std::path::Path;
use std::process::{Command, Output};
fn shepherd(root: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(args)
.arg("--root")
.arg(root)
.output()
.unwrap()
}
#[test]
fn init_and_lane_create_only_the_focused_workspace() {
let project = tempfile::tempdir().unwrap();
let initialized = shepherd(project.path(), &["init"]);
assert!(
initialized.status.success(),
"{}",
String::from_utf8_lossy(&initialized.stderr)
);
assert!(project.path().join(".shepherd/.gitignore").is_file());
assert!(project.path().join(".shepherd/runs").is_dir());
let lane = shepherd(
project.path(),
&["lane", "--run", "release-1", "--name", "cli"],
);
assert!(
lane.status.success(),
"{}",
String::from_utf8_lossy(&lane.stderr)
);
let lane_root = project.path().join(".shepherd/runs/release-1/lanes/cli");
assert!(lane_root.join("plan.md").is_file());
assert!(lane_root.join("ledger.md").is_file());
assert!(lane_root.join("evidence").is_dir());
assert!(
std::fs::read_to_string(lane_root.join("ledger.md"))
.unwrap()
.contains("Status: pending")
);
assert!(shepherd(project.path(), &["init"]).status.success());
assert!(
shepherd(
project.path(),
&["lane", "--run", "release-1", "--name", "cli"]
)
.status
.success()
);
}
#[test]
fn lane_requires_init_and_safe_identifiers() {
let project = tempfile::tempdir().unwrap();
let missing = shepherd(project.path(), &["lane", "--run", "r1", "--name", "docs"]);
assert!(!missing.status.success());
assert!(String::from_utf8_lossy(&missing.stderr).contains("is not initialized"));
assert!(shepherd(project.path(), &["init"]).status.success());
for args in [
["lane", "--run", "../escape", "--name", "docs"],
["lane", "--run", "r1", "--name", "a/b"],
] {
let invalid = shepherd(project.path(), &args);
assert!(!invalid.status.success());
assert!(String::from_utf8_lossy(&invalid.stderr).contains("must be 1-80 ASCII"));
}
assert!(!project.path().join("escape").exists());
}
#[test]
fn generated_files_are_never_overwritten() {
let project = tempfile::tempdir().unwrap();
assert!(shepherd(project.path(), &["init"]).status.success());
let ignore = project.path().join(".shepherd/.gitignore");
std::fs::write(&ignore, "custom\n").unwrap();
let conflict = shepherd(project.path(), &["init"]);
assert!(!conflict.status.success());
assert_eq!(std::fs::read_to_string(ignore).unwrap(), "custom\n");
}
#[test]
#[cfg(unix)]
fn workspace_paths_cannot_follow_symlinks() {
use std::os::unix::fs::symlink;
let project = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
symlink(outside.path(), project.path().join(".shepherd")).unwrap();
let result = shepherd(project.path(), &["init"]);
assert!(!result.status.success());
assert!(String::from_utf8_lossy(&result.stderr).contains("linked"));
assert_eq!(std::fs::read_dir(outside.path()).unwrap().count(), 0);
}
#[test]
fn edited_lane_records_and_evidence_survive_reinitialization() {
let project = tempfile::tempdir().unwrap();
assert!(shepherd(project.path(), &["init"]).status.success());
let args = ["lane", "--run", "r1", "--name", "508-custody.12"];
assert!(shepherd(project.path(), &args).status.success());
let lane = project.path().join(".shepherd/runs/r1/lanes/508-custody.12");
let ledger = std::fs::read_to_string(lane.join("ledger.md")).unwrap();
for field in ["Git common directory:", "Full branch ref:", "Base ref and SHA:",
"Integration target:", "Tested candidate SHA:", "Accepted plan revision:"] {
assert!(ledger.contains(field), "missing custody field {field}");
}
std::fs::write(lane.join("plan.md"), "accepted edited plan\n").unwrap();
std::fs::write(lane.join("ledger.md"), "live owner and candidate\n").unwrap();
std::fs::write(lane.join("evidence/preserved.txt"), "precious evidence\n").unwrap();
assert!(!shepherd(project.path(), &args).status.success());
assert_eq!(std::fs::read_to_string(lane.join("plan.md")).unwrap(), "accepted edited plan\n");
assert_eq!(std::fs::read_to_string(lane.join("ledger.md")).unwrap(), "live owner and candidate\n");
assert_eq!(std::fs::read_to_string(lane.join("evidence/preserved.txt")).unwrap(), "precious evidence\n");
}