use assert_cmd::Command;
use predicates::str::contains;
use provenance_core::SUPPORTED_SCHEMA_VERSION;
use std::path::Path;
fn requirements() -> String {
[
serde_json::json!({"schema_version": SUPPORTED_SCHEMA_VERSION.0,"scope_id":"default","id":"req_root","statement":"The root requirement","status":"active","source_refs":[{"source_id":"source_missing","clause":null}]}).to_string(),
serde_json::json!({"schema_version": SUPPORTED_SCHEMA_VERSION.0,"scope_id":"default","id":"req_child","statement":"The child requirement","status":"active","refines":"req_missing"}).to_string(),
]
.join("\n")
}
fn rules() -> String {
serde_json::json!({"schema_version": SUPPORTED_SCHEMA_VERSION.0,"scope_id":"default","id":"rule_root","statement":"The root rule","status":"active","severity":"high","requirement_ids":["req_root"],"resolution_ids":["res_missing"]}).to_string()
}
fn topics() -> String {
serde_json::json!({"schema_version": SUPPORTED_SCHEMA_VERSION.0,"scope_id":"default","id":"topic_root","requirement_id":"req_root","title":"Root topic","status":"explored","links":[{"target_type":"rule","target_id":"rule_missing"}]}).to_string()
}
fn threads() -> String {
serde_json::json!({"schema_version": SUPPORTED_SCHEMA_VERSION.0,"scope_id":"default","id":"thread_orphan","parent":{"node_type":"requirement","node_id":"req_vanished"},"status":"active","created_at":1}).to_string()
}
fn planted_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
init(dir.path());
let scope = dir.path().join(".provenance/state/scopes/default");
write_jsonl(&scope.join("requirements/req.jsonl"), &requirements());
write_jsonl(&scope.join("rules/rule.jsonl"), &rules());
write_jsonl(&scope.join("topics/topic.jsonl"), &topics());
write_jsonl(&scope.join("threads/threads.jsonl"), &threads());
dir
}
#[test]
fn gaps_name_every_dangling_target_by_its_declaration() {
let dir = planted_repo();
let output = Command::cargo_bin("provenance")
.unwrap()
.args([
"gaps",
"--repo",
dir.path().to_str().unwrap(),
"--scope",
"default",
"--format",
"json",
])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let gaps: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout).unwrap();
let dangling: Vec<(String, String, String)> = gaps
.iter()
.filter(|gap| gap["kind"] == "dangling_reference")
.map(|gap| {
(
gap["node_type"].as_str().unwrap().to_string(),
gap["node_id"].as_str().unwrap().to_string(),
gap["reason"].as_str().unwrap().to_string(),
)
})
.collect();
let row =
|kind: &str, id: &str, reason: &str| (kind.to_string(), id.to_string(), reason.to_string());
assert_eq!(
dangling,
vec![
row(
"requirement",
"req_root",
"cites points at missing source source_missing"
),
row(
"requirement",
"req_child",
"refines points at missing requirement req_missing"
),
row(
"rule",
"rule_root",
"resolution_ids points at missing resolution res_missing"
),
row(
"topic",
"topic_root",
"links points at missing rule rule_missing"
),
row(
"requirement",
"req_vanished",
"thread thread_orphan points at missing requirement req_vanished"
),
]
);
}
#[test]
fn check_refuses_every_dangling_target() {
let dir = planted_repo();
Command::cargo_bin("provenance")
.unwrap()
.args([
"check",
"--repo",
dir.path().to_str().unwrap(),
"--format",
"json",
])
.assert()
.failure()
.stderr(contains(
"requirement req_child has dangling reference: refines requirement req_missing",
))
.stderr(contains(
"requirement req_root has dangling reference: cites source source_missing",
))
.stderr(contains(
"rule rule_root has dangling reference: resolution_ids resolution res_missing",
))
.stderr(contains(
"topic topic_root has dangling reference: link rule rule_missing",
))
.stderr(contains(
"thread thread_orphan has dangling reference: parent requirement req_vanished",
));
}
fn init(repo: &Path) {
Command::cargo_bin("provenance")
.unwrap()
.args([
"init",
"--path",
repo.to_str().unwrap(),
"--scope",
"default",
"--path-prefix",
".",
])
.assert()
.success();
}
fn write_jsonl(path: &Path, records: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, format!("{records}\n")).unwrap();
}