use std::fs;
use std::path::Path;
use std::process::Command;
fn bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_spec-spine"))
}
fn code(out: &std::process::Output) -> i32 {
out.status.code().unwrap_or(-1)
}
fn write(root: &Path, rel: &str, content: &str) {
let p = root.join(rel);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, content).unwrap();
}
fn run(root: &Path, args: &[&str]) -> std::process::Output {
bin().arg("--repo").arg(root).args(args).output().unwrap()
}
#[test]
fn init_is_idempotent_and_force_overwrites() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let first = run(root, &["init"]);
assert_eq!(code(&first), 0);
assert!(root.join("specs/000-bootstrap/spec.md").is_file());
assert!(root.join("standards/spec/constitution.md").is_file());
assert!(
root.join(".claude/rules/adversarial-prompt-refusal.md")
.is_file()
);
let second = run(root, &["init"]);
assert_eq!(code(&second), 0);
assert!(String::from_utf8_lossy(&second.stdout).contains("skip (exists)"));
let forced = run(root, &["init", "--force"]);
assert_eq!(code(&forced), 0);
assert!(String::from_utf8_lossy(&forced.stdout).contains("(--force)"));
}
#[test]
fn adoption_loop_with_non_default_namespace_and_domains() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
write(
root,
"spec-spine.toml",
"[manifest]\nmetadata_namespace = \"acme\"\n\n[domains]\nallowed = [\"tooling\"]\n",
);
assert_eq!(code(&run(root, &["init"])), 0);
write(root, "Cargo.toml", "[workspace]\nmembers = [\"tool-x\"]\n");
write(
root,
"tool-x/Cargo.toml",
"[package]\nname = \"tool-x\"\nversion = \"0.1.0\"\n\
[package.metadata.acme]\nspec = \"010-feature\"\n",
);
write(root, "tool-x/src/lib.rs", "pub fn run() {}\n");
write(
root,
"specs/010-feature/spec.md",
"---\nid: \"010-feature\"\ntitle: \"Feature\"\nstatus: approved\n\
created: \"2026-06-09\"\nsummary: \"s\"\ndomain: \"tooling\"\n\
establishes:\n - \"tool-x/src/lib.rs\"\n---\n# 010\n## body\n",
);
assert_eq!(code(&run(root, &["compile"])), 0, "compile clean");
let idx = run(root, &["index"]);
assert_eq!(code(&idx), 0, "index clean");
let pkg_shard =
fs::read_to_string(root.join(".derived/codebase-index/by-package/tool-x.json")).unwrap();
assert!(
pkg_shard.contains("\"specRef\": \"010-feature\""),
"acme namespace must link tool-x → 010-feature"
);
assert_eq!(code(&run(root, &["lint"])), 0, "lint runs");
write(root, "changed1.txt", "tool-x/src/lib.rs\n");
let drift = run(
root,
&[
"couple",
"--paths-from",
root.join("changed1.txt").to_str().unwrap(),
],
);
assert_eq!(
code(&drift),
1,
"{}",
String::from_utf8_lossy(&drift.stderr)
);
write(
root,
"changed2.txt",
"tool-x/src/lib.rs\nspecs/010-feature/spec.md\n",
);
let cleared = run(
root,
&[
"couple",
"--paths-from",
root.join("changed2.txt").to_str().unwrap(),
],
);
assert_eq!(code(&cleared), 0);
}
#[test]
fn custom_domain_allowlist_is_enforced_at_compile() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
write(
root,
"spec-spine.toml",
"[domains]\nallowed = [\"tooling\"]\n",
);
write(root, "Cargo.toml", "[workspace]\nmembers = []\n");
write(
root,
"specs/010-feature/spec.md",
"---\nid: \"010-feature\"\ntitle: \"F\"\nstatus: approved\ncreated: \"2026-06-09\"\n\
summary: \"s\"\ndomain: \"not-allowed\"\n---\n# 010\n",
);
let out = run(root, &["compile"]);
assert_eq!(code(&out), 1, "invalid domain must fail compile");
}