#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::tests_outside_test_module,
reason = "integration test: fail-fast unwrap/expect are idiomatic"
)]
use std::fs;
use std::path::Path;
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_doctrine");
fn write(root: &Path, rel: &str, body: &str) {
let path = root.join(rel);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, body).unwrap();
}
fn seed_fixture(root: &Path) {
write(
root,
".doctrine/slice/001/slice-001.toml",
"id = 1\nslug = \"s1\"\ntitle = \"S1\"\nstatus = \"proposed\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[[relation]]\nlabel = \"requirements\"\ntarget = \"REQ-005\"\n",
);
write(root, ".doctrine/slice/001/slice-001.md", "scope\n");
write(
root,
".doctrine/slice/003/slice-003.toml",
"id = 3\nslug = \"s3\"\ntitle = \"S3\"\nstatus = \"proposed\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n",
);
write(root, ".doctrine/slice/003/slice-003.md", "scope\n");
write(
root,
".doctrine/adr/002/adr-002.toml",
"id = 2\nslug = \"a2\"\ntitle = \"A2\"\nstatus = \"accepted\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[relationships]\nsupersedes = [\"ADR-001\"]\n",
);
write(root, ".doctrine/adr/002/adr-002.md", "body\n");
write(
root,
".doctrine/requirement/005/requirement-005.toml",
"id = 5\nslug = \"r5\"\ntitle = \"R5\"\nstatus = \"active\"\n",
);
write(root, ".doctrine/requirement/005/requirement-005.md", "r\n");
}
fn stdout(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
#[test]
fn catalog_scan_json_valid() {
let tmp = tempfile::tempdir().unwrap();
seed_fixture(tmp.path());
let out = Command::new(BIN)
.args(["catalog", "scan", "--root"])
.arg(tmp.path())
.output()
.expect("spawn doctrine");
assert!(
out.status.success(),
"catalog scan failed: {}",
stderr(&out)
);
let v: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("valid JSON");
assert!(v.get("entities").is_some(), "missing entities key");
assert!(v.get("edges").is_some(), "missing edges key");
assert!(v.get("diagnostics").is_some(), "missing diagnostics key");
assert_eq!(
v["entities"].as_array().unwrap().len(),
4,
"expected 4 entities"
);
assert_eq!(v["edges"].as_array().unwrap().len(), 2, "expected 2 edges");
assert_eq!(
v["diagnostics"].as_array().unwrap().len(),
1,
"expected 1 diagnostic (ADR-002→ADR-001 unresolved)"
);
}
#[test]
fn catalog_graph_json_valid() {
let tmp = tempfile::tempdir().unwrap();
seed_fixture(tmp.path());
let out = Command::new(BIN)
.args(["catalog", "graph", "--root"])
.arg(tmp.path())
.output()
.expect("spawn doctrine");
assert!(
out.status.success(),
"catalog graph failed: {}",
stderr(&out)
);
let v: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("valid JSON");
assert!(v.get("nodes").is_some(), "missing nodes key");
assert!(v.get("edges").is_some(), "missing edges key");
assert_eq!(v["nodes"].as_object().unwrap().len(), 4, "expected 4 nodes");
assert_eq!(v["edges"].as_array().unwrap().len(), 2, "expected 2 edges");
}
#[test]
fn catalog_scan_nonexistent_root_exits_nonzero() {
let out = Command::new(BIN)
.args(["catalog", "scan", "--root", "/nonexistent"])
.output()
.expect("spawn doctrine");
assert!(!out.status.success(), "expected non-zero exit");
assert!(!stderr(&out).is_empty(), "expected stderr message on error");
}