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 show(root: &Path, extra: &[&str]) -> std::process::Output {
let mut cmd = bin();
cmd.arg("--repo").arg(root).args(["config", "show"]);
cmd.args(extra);
cmd.output().unwrap()
}
fn show_json(root: &Path) -> serde_json::Value {
let out = show(root, &["--json"]);
assert_eq!(code(&out), 0, "{}", String::from_utf8_lossy(&out.stderr));
serde_json::from_slice(&out.stdout).expect("a single JSON object")
}
#[test]
fn it_reports_on_a_repository_that_never_compiled() {
let tmp = tempfile::tempdir().unwrap();
fs::create_dir_all(tmp.path().join("specs")).unwrap();
let out = show(tmp.path(), &[]);
assert_eq!(code(&out), 0, "{}", String::from_utf8_lossy(&out.stderr));
assert!(String::from_utf8_lossy(&out.stdout).contains("config_version"));
}
#[test]
fn it_never_writes_a_config_file() {
let tmp = tempfile::tempdir().unwrap();
fs::create_dir_all(tmp.path().join("specs")).unwrap();
assert_eq!(code(&show(tmp.path(), &[])), 0);
assert!(
!tmp.path().join("spec-spine.toml").exists(),
"a read verb must not scaffold"
);
}
#[test]
fn the_bypass_floor_is_merged_ordered_and_attributed() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"spec-spine.toml",
"[coupling]\nbypass_prefixes = [\"vendor/\", \"**/README.md\"]\n",
);
let v = show_json(tmp.path());
let entries = v["coupling"]["bypass_prefixes"].as_array().unwrap();
let built_in: Vec<&str> = spec_spine_core::DEFAULT_BYPASS_PREFIXES.to_vec();
assert!(
entries.len() > built_in.len(),
"the adopter's entries join the floor"
);
for (i, prefix) in built_in.iter().enumerate() {
assert_eq!(entries[i]["prefix"], *prefix, "floor order at {i}");
assert_eq!(entries[i]["sources"][0], "built-in");
}
let tail: Vec<&str> = entries[built_in.len()..]
.iter()
.map(|e| e["prefix"].as_str().unwrap())
.collect();
assert_eq!(tail, vec!["vendor/", "**/README.md"]);
for e in &entries[built_in.len()..] {
assert_eq!(e["sources"][0], "spec-spine.toml");
}
}
#[test]
fn a_prefix_in_both_lists_appears_once_with_both_sources() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"spec-spine.toml",
"[coupling]\nbypass_prefixes = [\"docs/\"]\n",
);
let v = show_json(tmp.path());
let entries = v["coupling"]["bypass_prefixes"].as_array().unwrap();
let docs: Vec<&serde_json::Value> = entries.iter().filter(|e| e["prefix"] == "docs/").collect();
assert_eq!(docs.len(), 1, "reported once: {docs:?}");
let sources: Vec<&str> = docs[0]["sources"]
.as_array()
.unwrap()
.iter()
.map(|s| s.as_str().unwrap())
.collect();
assert_eq!(sources, vec!["built-in", "spec-spine.toml"]);
}
#[test]
fn the_json_form_is_not_a_verdict_envelope() {
let tmp = tempfile::tempdir().unwrap();
let v = show_json(tmp.path());
assert!(v.get("ok").is_none(), "no verdict members: {v}");
assert!(v.get("exitCode").is_none(), "{v}");
assert!(v.get("verb").is_none(), "{v}");
assert_eq!(v["config_version"], "0.1.0", "§3.4: the shape's version");
}
#[test]
fn every_default_is_resolved_and_reported() {
let tmp = tempfile::tempdir().unwrap();
let v = show_json(tmp.path());
for table in [
"manifest",
"domains",
"kind",
"layout",
"index",
"branding",
"coupling",
"provenance",
"frontmatter",
"lint",
] {
assert!(v.get(table).is_some(), "missing table '{table}': {v}");
}
assert_eq!(v["layout"]["cargo_workspace"], "Cargo.toml");
assert_eq!(v["index"]["resolver_exclusions"][0], "target");
assert_eq!(v["manifest"]["metadata_namespace"], "spec-spine");
}
#[test]
fn the_report_covers_every_config_table() {
let cfg = serde_json::to_value(spec_spine_types::Config::default()).unwrap();
let effective = serde_json::to_value(spec_spine_types::EffectiveConfig::new(
&spec_spine_types::Config::default(),
Vec::new(),
))
.unwrap();
let mut want: Vec<&str> = cfg
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
want.sort_unstable();
let mut got: Vec<&str> = effective
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.filter(|k| *k != "config_version")
.collect();
got.sort_unstable();
assert_eq!(got, want, "every Config table must be reported");
}
#[test]
fn the_output_is_deterministic() {
let tmp = tempfile::tempdir().unwrap();
write(
tmp.path(),
"spec-spine.toml",
"[coupling]\nbypass_prefixes = [\"vendor/\"]\n",
);
let a = show(tmp.path(), &["--json"]).stdout;
let b = show(tmp.path(), &["--json"]).stdout;
assert_eq!(a, b);
assert!(a.ends_with(b"\n"), "trailing newline");
}
#[test]
fn a_malformed_config_is_exit_3() {
let tmp = tempfile::tempdir().unwrap();
write(tmp.path(), "spec-spine.toml", "[coupling]\nnot_a_key = 1\n");
assert_eq!(code(&show(tmp.path(), &[])), 3);
}