mod common;
use common::Sandbox;
use std::fs;
use std::process::Command;
#[test]
fn req_0075_init_directory_layout_writes_index_and_requirements_dir() {
let s = Sandbox::new();
let dir = s.dir.path().join("proj");
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.args([
"init",
"-n",
"dir-proj",
"-o",
dir.to_str().unwrap(),
"--layout",
"directory",
])
.output()
.expect("init");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(dir.join("index.req").exists());
assert!(dir.join("requirements").is_dir());
}
#[test]
fn req_0075_add_persists_one_file_per_requirement() {
let s = Sandbox::new();
let dir = s.dir.path().join("proj");
Command::new(env!("CARGO_BIN_EXE_req"))
.args([
"init",
"-n",
"dir-proj",
"-o",
dir.to_str().unwrap(),
"--layout",
"directory",
])
.output()
.expect("init");
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.args([
"--file",
dir.to_str().unwrap(),
"add",
"--title",
"Persisted under the directory layout here",
"--statement",
"The system shall write this requirement to its own file under requirements/.",
"--rationale",
"Test fixture.",
"--kind",
"constraint",
"--priority",
"could",
])
.output()
.expect("add");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
dir.join("requirements/REQ-0001.req").exists(),
"REQ-0001.req should exist under requirements/"
);
}
#[test]
fn req_0075_integrity_detects_per_file_tamper() {
let s = Sandbox::new();
let dir = s.dir.path().join("proj");
Command::new(env!("CARGO_BIN_EXE_req"))
.args([
"init",
"-n",
"dir-proj",
"-o",
dir.to_str().unwrap(),
"--layout",
"directory",
])
.output()
.expect("init");
Command::new(env!("CARGO_BIN_EXE_req"))
.args([
"--file",
dir.to_str().unwrap(),
"add",
"--title",
"Will be tampered with in this test",
"--statement",
"The system shall persist this so we can mutate the file.",
"--rationale",
"Test.",
"--kind",
"constraint",
"--priority",
"could",
])
.output()
.expect("add");
let req_path = dir.join("requirements/REQ-0001.req");
let text = fs::read_to_string(&req_path).unwrap();
fs::write(&req_path, text.replace("\"Could\"", "\"Should\"")).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.args(["--file", dir.to_str().unwrap(), "list"])
.output()
.expect("list");
assert!(
!out.status.success(),
"list should refuse after per-file tamper"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("integrity check failed"));
}
#[test]
fn req_0076_near_clone_triggers_dup_intent_warning() {
let s = Sandbox::new();
s.init("p");
let _ = s.run(&[
"add",
"--title",
"Persist user sessions across restarts forever",
"--statement",
"The system shall persist user sessions across process restarts.",
"--rationale",
"Users lose work today.",
"--kind",
"functional",
"--priority",
"should",
"--accept",
"Session survives restart in fixture",
]);
let _ = s.run(&[
"add",
"--title",
"Persist user sessions across process restarts",
"--statement",
"The system shall persist user sessions across process restarts always.",
"--rationale",
"Same intent, different words.",
"--kind",
"functional",
"--priority",
"should",
"--accept",
"Session survives restart in fixture as well",
]);
let out = s.run(&["validate"]);
let text = String::from_utf8_lossy(&out.stdout);
assert!(
text.contains("REQ-V-0020"),
"expected duplicate-intent warning, got:\n{}",
text
);
}
#[test]
fn req_0077_verifies_link_without_test_record_warns() {
let s = Sandbox::new();
s.init("p");
for i in 1..=2 {
s.run(&[
"add",
"--title",
&format!("Subject of the verification {}", i),
"--statement",
"The system shall have this perfectly fine baseline behaviour.",
"--rationale",
"Setup.",
"--kind",
"constraint",
"--priority",
"could",
]);
}
s.run(&["link", "REQ-0002", "REQ-0001", "-k", "verifies"]);
for status in ["proposed", "approved", "implemented"] {
s.run(&[
"update",
"REQ-0002",
"--status",
status,
"--reason",
"test setup",
]);
}
let out = s.run(&["validate"]);
let text = String::from_utf8_lossy(&out.stdout);
assert!(
text.contains("REQ-V-0019"),
"expected verifies-without-evidence warning, got:\n{}",
text
);
}
#[test]
fn req_0078_schema_add_is_valid_json_with_format() {
let out = common::req(&["schema", "add"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("schema add is JSON");
assert_eq!(
v["$schema"].as_str().unwrap(),
"https://json-schema.org/draft/2020-12/schema"
);
assert!(
v["$id"]
.as_str()
.unwrap()
.starts_with("urn:req-cli:schema:"),
"schema $id should be a stable urn:, got: {}",
v["$id"]
);
assert!(v["properties"]["title"].is_object());
assert!(v["properties"]["statement"].is_object());
assert_eq!(v["_format"].as_str().unwrap(), "req-v3");
}
#[test]
fn req_0127_coverage_by_req_groups_files_under_each_req() {
let s = Sandbox::new();
s.init("p");
std::fs::create_dir_all(s.dir.path().join("src")).unwrap();
std::fs::write(
s.dir.path().join("src/a.rs"),
"// REQ-0001: first\nfn a() {}\n",
)
.unwrap();
std::fs::write(
s.dir.path().join("src/b.rs"),
"// REQ-0001: also here\nfn b() {}\n",
)
.unwrap();
std::fs::write(
s.dir.path().join("src/c.rs"),
"// REQ-0002: somewhere else\nfn c() {}\n",
)
.unwrap();
let out = s.run(&[
"coverage",
"--by-req",
"--path",
s.dir.path().to_str().unwrap(),
"--json",
]);
let v: serde_json::Value = serde_json::from_str(&common::stdout(&out)).expect("JSON");
let r1 = v["REQ-0001"].as_array().expect("REQ-0001 array");
assert_eq!(
r1.len(),
2,
"REQ-0001 should reference two files; got {:?}",
r1
);
let r2 = v["REQ-0002"].as_array().expect("REQ-0002 array");
assert_eq!(
r2.len(),
1,
"REQ-0002 should reference one file; got {:?}",
r2
);
}
#[test]
fn req_0121_coverage_reports_drafts_unmarked_separately() {
let s = Sandbox::new();
s.init("p");
s.run(&[
"add",
"--title",
"Draft with no marker yet",
"--statement",
"The system shall implement this once we get to it.",
"--rationale",
"Fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
s.run(&[
"add",
"--title",
"Implemented but missing marker",
"--statement",
"The system shall carry this real obligation right now.",
"--rationale",
"Fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
let _ = s.run(&[
"update",
"REQ-0002",
"--status",
"implemented",
"--reason",
"fixture: forced past Draft to test orphans bucket",
"--force",
]);
let out = s.run(&[
"coverage",
"--path",
s.dir.path().to_str().unwrap(),
"--json",
]);
let v: serde_json::Value = serde_json::from_str(&common::stdout(&out)).expect("JSON");
let orphans = v["orphans"].as_array().expect("orphans array");
let drafts = v["drafts_unmarked"]
.as_array()
.expect("drafts_unmarked array");
assert!(
orphans.iter().any(|x| x == "REQ-0002"),
"REQ-0002 (Implemented, no marker) should be an orphan; got: {:?}",
orphans
);
assert!(
drafts.iter().any(|x| x == "REQ-0001"),
"REQ-0001 (Draft, no marker) should be in drafts_unmarked; got: {:?}",
drafts
);
}
#[test]
fn req_0120_installed_agents_uses_placeholder_req_ids() {
let s = Sandbox::new();
s.init("p");
let agents = s.dir.path().join("AGENTS.md");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(s.dir.path())
.args(["help", "agents", "--install"])
.output()
.expect("install agents");
assert!(
out.status.success(),
"install: {}",
String::from_utf8_lossy(&out.stderr)
);
let body = std::fs::read_to_string(&agents).expect("read AGENTS.md");
let re = regex::Regex::new(r"REQ-\d{4}").unwrap();
let leaked: Vec<&str> = re.find_iter(&body).map(|m| m.as_str()).collect();
assert!(
leaked.is_empty(),
"installed AGENTS.md must not carry literal REQ-NNNN; found: {:?}",
leaked
);
assert!(
body.contains("REQ-NNNN"),
"expected placeholder REQ-NNNN to appear in the installed text"
);
}
#[test]
fn req_0119_import_schema_requires_rationale() {
let out = common::req(&["schema", "import"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("schema import is JSON");
let required = v["items"]["required"]
.as_array()
.expect("items.required is an array");
let required_strs: Vec<&str> = required.iter().filter_map(|x| x.as_str()).collect();
assert!(
required_strs.contains(&"rationale"),
"import schema must list rationale as required (the validator does); got: {:?}",
required_strs
);
}
#[test]
fn req_0078_schema_batch_describes_oneof_mutations() {
let out = common::req(&["schema", "batch"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("schema batch is JSON");
let mutations = &v["properties"]["mutations"]["items"]["oneOf"];
assert!(
mutations.is_array(),
"batch schema should describe mutation alternatives"
);
assert_eq!(mutations.as_array().unwrap().len(), 4);
}
#[test]
fn req_0079_audit_gate_exits_nonzero_without_signing() {
let s = Sandbox::new();
s.init("p");
let dir = s.dir.path();
let _ = std::process::Command::new("git")
.current_dir(dir)
.args(["init", "-q", "-b", "main"])
.output();
let _ = std::process::Command::new("git")
.current_dir(dir)
.args(["config", "user.email", "t@example.com"])
.output();
let _ = std::process::Command::new("git")
.current_dir(dir)
.args(["config", "user.name", "T"])
.output();
let _ = std::process::Command::new("git")
.current_dir(dir)
.args(["add", "project.req"])
.output();
let _ = std::process::Command::new("git")
.current_dir(dir)
.args(["-c", "commit.gpgsign=false", "commit", "-q", "-m", "init"])
.output();
let out = std::process::Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(dir)
.args([
"--file",
s.path().to_str().unwrap(),
"audit",
"--gate",
"--require-good-signature",
])
.output()
.expect("audit gate");
assert!(!out.status.success(), "unsigned commit should violate gate");
}
#[test]
fn req_0080_changelog_exists_with_unreleased_section() {
let text = fs::read_to_string("CHANGELOG.md").expect("CHANGELOG.md present");
assert!(text.contains("# Changelog"));
assert!(text.contains("[Unreleased]"));
assert!(text.to_lowercase().contains("keep a changelog"));
}