mod common;
use common::{stderr, stdout, Sandbox};
use std::fs;
use std::process::Command;
fn init_git_repo(path: &std::path::Path) {
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(path)
.output()
.expect("git init");
let _ = Command::new("git")
.args(["config", "user.email", "t@e"])
.current_dir(path)
.output();
let _ = Command::new("git")
.args(["config", "user.name", "t"])
.current_dir(path)
.output();
let _ = Command::new("git")
.args(["config", "commit.gpgsign", "false"])
.current_dir(path)
.output();
}
fn git_commit(path: &std::path::Path, msg: &str) {
let _ = Command::new("git")
.args(["add", "-A"])
.current_dir(path)
.output();
let _ = Command::new("git")
.args(["commit", "-q", "-m", msg])
.current_dir(path)
.output();
}
#[test]
fn req_0112_record_carries_content_hash_when_marker_present() {
let s = Sandbox::new();
s.init("p");
init_git_repo(s.dir.path());
let out = s.run(&[
"add",
"--title",
"Hashed requirement here",
"--statement",
"The system shall be referenced from a source file with a marker.",
"--rationale",
"Content-hash fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
assert!(out.status.success(), "add: {}", stderr(&out));
fs::create_dir_all(s.dir.path().join("src")).unwrap();
fs::write(
s.dir.path().join("src/lib.rs"),
"// REQ-0001: this implementation\nfn _ok() {}\n",
)
.unwrap();
git_commit(s.dir.path(), "initial");
let abs = s.dir.path().to_path_buf();
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(&abs)
.args([
"--file",
s.path().to_str().unwrap(),
"test",
"record",
"REQ-0001",
"--result",
"pass",
"--notes",
"content-hash test",
])
.output()
.expect("test record");
assert!(
out.status.success(),
"test record: {}",
String::from_utf8_lossy(&out.stderr)
);
let show = stdout(&s.run(&["show", "REQ-0001", "--json"]));
assert!(
show.contains("\"content_hash\""),
"record should carry content_hash: {}",
show
);
assert!(
show.contains("\"linked_files\""),
"record should carry linked_files: {}",
show
);
}
#[test]
fn req_0112_stale_fires_only_on_actual_content_change() {
let s = Sandbox::new();
s.init("p");
init_git_repo(s.dir.path());
let _ = s.run(&[
"add",
"--title",
"Stale tracking target",
"--statement",
"The system shall be content-stable across unrelated commits.",
"--rationale",
"Stale-flap fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
fs::create_dir_all(s.dir.path().join("src")).unwrap();
fs::write(
s.dir.path().join("src/lib.rs"),
"// REQ-0001: anchor\nfn _ok() {}\n",
)
.unwrap();
git_commit(s.dir.path(), "initial");
let abs = s.dir.path().to_path_buf();
let rec = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(&abs)
.args([
"--file",
s.path().to_str().unwrap(),
"test",
"record",
"REQ-0001",
"--result",
"pass",
"--notes",
"baseline",
])
.output()
.expect("test record");
assert!(
rec.status.success(),
"{}",
String::from_utf8_lossy(&rec.stderr)
);
fs::write(s.dir.path().join("README.md"), "unrelated change\n").unwrap();
git_commit(s.dir.path(), "unrelated");
let stale = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(&abs)
.args([
"--file",
s.path().to_str().unwrap(),
"stale",
"--path",
abs.to_str().unwrap(),
"--json",
])
.output()
.expect("stale");
let body = String::from_utf8_lossy(&stale.stdout);
assert!(
body.contains("\"stale\": 0") || body.contains("\"state\": \"fresh\""),
"content-hash should keep STALE quiet on unrelated commits: {}",
body
);
fs::write(
s.dir.path().join("src/lib.rs"),
"// REQ-0001: anchor\nfn _ok() { /* changed */ }\n",
)
.unwrap();
git_commit(s.dir.path(), "real change");
let stale2 = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(&abs)
.args([
"--file",
s.path().to_str().unwrap(),
"stale",
"--path",
abs.to_str().unwrap(),
"--json",
])
.output()
.expect("stale 2");
let body2 = String::from_utf8_lossy(&stale2.stdout);
assert!(
body2.contains("\"STALE\"") || body2.contains("\"stale\": 1"),
"content-hash should fire STALE when linked file content changes: {}",
body2
);
}
#[test]
fn req_0112_old_record_without_hash_falls_back_to_sha() {
let s = Sandbox::new();
s.init("p");
let out = s.run(&["stale", "--json"]);
assert!(
out.status.success(),
"stale should succeed on empty: {}",
stderr(&out)
);
}
#[test]
fn req_0153_refresh_never_rehashes_drifted_source() {
let s = Sandbox::new();
s.init("p");
init_git_repo(s.dir.path());
let abs = s.dir.path().to_path_buf();
let file = s.path().to_str().unwrap().to_string();
let run_in = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(&abs)
.args(["--file", &file])
.args(args)
.output()
.expect("req")
};
run_in(&[
"add",
"--title",
"Hashed requirement here",
"--statement",
"The system shall be referenced from a marked source file.",
"--rationale",
"Refresh fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
fs::create_dir_all(abs.join("src")).unwrap();
fs::write(abs.join("src/lib.rs"), "// REQ-0001: impl\nfn a() {}\n").unwrap();
git_commit(&abs, "init");
for st in ["proposed", "approved", "implemented"] {
run_in(&["update", "REQ-0001", "--status", st, "--reason", "x"]);
}
run_in(&["verification", "plan", "REQ-0001", "--plan", "review"]);
run_in(&[
"verification",
"analysis",
"REQ-0001",
"--findings",
"ok",
"--result",
"pass",
]);
run_in(&[
"verification",
"test",
"REQ-0001",
"--findings",
"ok",
"--result",
"pass",
]);
let c = run_in(&[
"verification",
"conclude",
"REQ-0001",
"--statement",
"met",
"--promote",
]);
assert!(
c.status.success(),
"conclude: {}",
String::from_utf8_lossy(&c.stderr)
);
let fresh = run_in(&["verification", "refresh-anchors", "--path", ".", "--json"]);
let jf = String::from_utf8_lossy(&fresh.stdout);
assert!(
jf.contains("\"refreshed\": []"),
"a fresh anchor must not be refreshed: {jf}"
);
fs::write(
abs.join("src/lib.rs"),
"// REQ-0001: impl\nfn a() { let _x = 1; }\n",
)
.unwrap();
let out = run_in(&["verification", "refresh-anchors", "--path", ".", "--json"]);
let j = String::from_utf8_lossy(&out.stdout);
assert!(
j.contains("\"refreshed\": []"),
"a genuinely drifted source must never be refreshed: {j}"
);
assert!(
j.contains("REQ-0001"),
"the drifted requirement should be reported: {j}"
);
}