mod common;
use common::{req, stderr, stdout, Sandbox};
#[test]
fn req_0134_sil_derives_and_propagates_through_the_chain() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
assert!(s
.run(&["hazard", "add", "-t", "H", "--harm", "someone is hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"])
.status
.success());
assert!(stdout(&s.run(&["hazard", "list"])).contains("SIL3"));
assert!(s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]).status.success());
assert!(stdout(&s.run(&["sf", "list"])).contains("SIL3"), "SF allocates SIL3");
assert!(s
.run(&["sreq", "add", "-t", "R", "-s", "The system shall stop.", "-r", "because", "-a", "stops", "--realizes", "SF-0001"])
.status
.success());
assert!(stdout(&s.run(&["sreq", "list"])).contains("SIL3"), "SR inherits SIL3");
}
#[test]
fn req_0135_sil_gate_blocks_inspection_and_force_needs_reason() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
s.run(&["hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"]);
s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
s.run(&["sreq", "add", "-t", "Stop the blade", "-s", "The system shall stop the blade on demand.", "-r", "Operator safety during cleaning.", "-a", "blade stops within 200ms", "--realizes", "SF-0001"]);
s.run(&["sreq", "update", "SR-0001", "--status", "approved", "--reason", "r"]);
s.run(&["sreq", "update", "SR-0001", "--status", "implemented", "--reason", "r"]);
let blocked = s.run(&["sreq", "verify", "SR-0001", "--by", "inspection", "--promote"]);
assert!(!blocked.status.success(), "SIL3 inspection promote must be blocked");
assert!(stderr(&blocked).contains("SIL-rigour gate"));
let no_reason = s.run(&["sreq", "verify", "SR-0001", "--by", "inspection", "--promote", "--force"]);
assert!(!no_reason.status.success(), "--force without --reason must fail");
let forced = s.run(&["sreq", "verify", "SR-0001", "--by", "inspection", "--promote", "--force", "--reason", "accepted at design review"]);
assert!(forced.status.success(), "stderr={}", stderr(&forced));
let shown = stdout(&s.run(&["sreq", "show", "SR-0001", "--json"]));
let v: serde_json::Value = serde_json::from_str(&shown).expect("json");
let last = v["tests"].as_array().unwrap().last().unwrap();
assert_eq!(last["sil_gate_exception"], true, "structured exception flag set");
assert_eq!(v["status"], "Verified");
}
#[test]
fn req_0135_recording_inspection_without_promote_is_allowed() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
s.run(&["hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"]);
s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
s.run(&["sreq", "add", "-t", "Stop the blade", "-s", "The system shall stop the blade on demand.", "-r", "Operator safety during cleaning.", "-a", "blade stops within 200ms", "--realizes", "SF-0001"]);
let out = s.run(&["sreq", "verify", "SR-0001", "--by", "inspection"]);
assert!(out.status.success(), "non-promoting inspection record must be allowed: {}", stderr(&out));
}
#[test]
fn req_0135_obsolete_hazard_drops_from_allocation() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
s.run(&["hazard", "add", "-t", "High", "--harm", "killed", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"]); s.run(&["hazard", "add", "-t", "Low", "--harm", "minor", "-C", "C_B", "-F", "F_A", "-P", "P_A", "-W", "W3"]); s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001", "--mitigates", "HAZ-0002"]);
assert!(stdout(&s.run(&["sf", "list"])).contains("SIL3"), "allocated = max = SIL3");
s.run(&["hazard", "update", "HAZ-0001", "--status", "obsolete", "--reason", "reclassified"]);
assert!(!stdout(&s.run(&["sf", "list"])).contains("SIL3"), "obsolete hazard must no longer drive allocation");
}
#[test]
fn req_0135_directory_layout_persists_safety_artifacts() {
let dir = tempfile::Builder::new().prefix("req-dir-").tempdir().unwrap();
let proj = dir.path().join("proj");
let p = proj.to_str().unwrap();
assert!(req(&["init", "-n", "d", "-o", p, "--layout", "directory"]).status.success());
common::enable_safety(std::path::Path::new(p));
assert!(req(&["--file", p, "hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_D", "-F", "F_B", "-P", "P_B", "-W", "W3"]).status.success());
let listed = req(&["--file", p, "hazard", "list"]);
assert!(listed.status.success(), "{}", stderr(&listed));
assert!(stdout(&listed).contains("HAZ-0001"), "hazard must survive a directory-layout round trip");
assert!(req(&["--file", p, "validate"]).status.success(), "directory integrity must hold after a safety write");
}
#[test]
fn req_0136_trace_is_honest_about_what_it_asserts() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
s.run(&["hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"]);
s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
s.run(&["sreq", "add", "-t", "Stop the blade", "-s", "The system shall stop the blade on demand.", "-r", "Operator safety during cleaning.", "-a", "blade stops within 200ms", "--realizes", "SF-0001"]);
let out = stdout(&s.run(&["trace", "HAZ-0001"]));
assert!(out.contains("TRACE STATUS"), "uses traceability wording, not 'safety case'");
assert!(!out.contains("SAFETY CASE"), "must not claim a safety-case verdict");
assert!(out.contains("not qualified per IEC 61508-3"), "carries the disclaimer");
}
#[test]
fn req_0137_wellformed_safety_chain_validates_clean() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
s.run(&["hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"]);
s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
s.run(&["sreq", "add", "-t", "Stop the blade", "-s", "The system shall stop the blade on demand.", "-r", "Operator safety during cleaning.", "-a", "blade stops within 200ms", "--realizes", "SF-0001"]);
let out = s.run(&["validate"]);
assert!(out.status.success(), "well-formed safety chain must validate: {}", stdout(&out));
}
#[test]
fn req_0135_sr_evidence_from_test_run_goes_stale_on_code_change() {
use std::process::Command;
let dir = tempfile::Builder::new().prefix("req-evh-").tempdir().unwrap();
let root = dir.path();
let bin = env!("CARGO_BIN_EXE_req");
let run = |args: &[&str]| {
Command::new(bin)
.args(args)
.current_dir(root)
.env_remove("REQ_FILE")
.output()
.expect("run req")
};
assert!(run(&["init", "-n", "p"]).status.success());
common::enable_safety(&root.join("project.req"));
run(&["hazard", "add", "-t", "Hazardous mode", "--harm", "operator hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"]);
run(&["sf", "add", "-t", "Interlock", "--mitigates", "HAZ-0001"]);
run(&["sreq", "add", "-t", "Cut blade power", "-s", "The interlock shall cut blade power within 200 ms.", "-r", "Bounds operator exposure.", "-a", "power cut <=200ms", "--realizes", "SF-0001"]);
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/interlock.rs"), "// SR-0001: interlock\nfn interlock() {}\n").unwrap();
std::fs::write(root.join("log.txt"), "running 1 test\ntest sr_0001_cuts_power ... ok\n").unwrap();
let tr = run(&["test", "run", "--from-file", "log.txt"]);
assert!(tr.status.success(), "test run: {}", String::from_utf8_lossy(&tr.stderr));
let shown = String::from_utf8_lossy(&run(&["sreq", "show", "SR-0001", "--json"]).stdout).into_owned();
let v: serde_json::Value = serde_json::from_str(&shown).expect("json");
let tests = v["tests"].as_array().expect("tests");
assert!(
tests.iter().any(|t| t["kind"] == "Automated"),
"SR must carry automated evidence from the run"
);
let fresh = run(&["stale", "--only-stale"]);
assert!(!String::from_utf8_lossy(&fresh.stdout).contains("SR-0001"), "should be fresh before any change");
std::fs::write(root.join("src/interlock.rs"), "// SR-0001: interlock\nfn interlock() { /* changed */ }\n").unwrap();
let stale = run(&["stale"]);
let out = String::from_utf8_lossy(&stale.stdout);
assert!(out.contains("SR-0001") && out.contains("STALE"), "SR evidence must go stale on code change:\n{}", out);
}
#[test]
fn req_0135_sr_coverage_orphans_and_ghosts() {
use std::process::Command;
let dir = tempfile::Builder::new().prefix("req-cov-").tempdir().unwrap();
let root = dir.path();
let bin = env!("CARGO_BIN_EXE_req");
let run = |args: &[&str]| {
Command::new(bin).args(args).current_dir(root).env_remove("REQ_FILE").output().expect("run req")
};
assert!(run(&["init", "-n", "p"]).status.success());
common::enable_safety(&root.join("project.req"));
run(&["hazard", "add", "-t", "Hazardous mode", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"]);
run(&["sf", "add", "-t", "Interlock", "--mitigates", "HAZ-0001"]);
run(&["sreq", "add", "-t", "Marked one", "-s", "The interlock shall cut blade power fast.", "-r", "safety", "-a", "cuts", "--realizes", "SF-0001"]);
run(&["sreq", "add", "-t", "Orphan one", "-s", "The guard shall be detected within 50 ms.", "-r", "safety", "-a", "detects", "--realizes", "SF-0001"]);
for sr in ["SR-0001", "SR-0002"] {
run(&["sreq", "update", sr, "--status", "approved", "--reason", "r"]);
run(&["sreq", "update", sr, "--status", "implemented", "--reason", "r"]);
}
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/x.rs"), "// SR-0001: here\n// SR-0099: ghost\nfn x() {}\n").unwrap();
let cov = run(&["coverage", "--path", "."]);
let out = String::from_utf8_lossy(&cov.stdout);
assert!(out.contains("SR-0002"), "SR-0002 should be an orphan:\n{}", out);
assert!(out.contains("SR-0099"), "SR-0099 should be a ghost:\n{}", out);
assert!(!out.contains("SR ORPHANS") || !out.contains("SR-0001\n SR-0001"), "SR-0001 is referenced, not an orphan");
assert!(!run(&["coverage", "--path", ".", "--strict"]).status.success(), "strict must fail on SR findings");
}
#[test]
fn req_0138_governance_gate_agent_refusal_and_calibration() {
use std::process::Command;
let dir = tempfile::Builder::new().prefix("req-gov-").tempdir().unwrap();
let root = dir.path();
let bin = env!("CARGO_BIN_EXE_req");
let run = |args: &[&str], kind: Option<&str>| {
let mut c = Command::new(bin);
c.args(args).current_dir(root).env_remove("REQ_FILE");
match kind {
Some(k) => {
c.env("REQ_ACTOR_KIND", k);
}
None => {
c.env_remove("REQ_ACTOR_KIND");
}
}
c.output().expect("run req")
};
assert!(run(&["init", "-n", "p"], None).status.success());
let blocked = run(&["hazard", "add", "-t", "H", "--harm", "x", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"], None);
assert!(!blocked.status.success(), "hazard add must be gated before acceptance");
assert!(String::from_utf8_lossy(&blocked.stderr).contains("not enabled"));
let agent = run(&["safety", "accept", "--name", "Bot"], Some("agent"));
assert!(!agent.status.success(), "agent must not be able to accept");
assert!(String::from_utf8_lossy(&agent.stderr).contains("human"));
let no_tty = run(&["safety", "accept", "--name", "Tom"], None);
assert!(!no_tty.status.success(), "accept must require a terminal");
assert!(String::from_utf8_lossy(&no_tty.stderr).contains("interactive terminal"));
common::enable_safety(&root.join("project.req"));
assert!(run(&["hazard", "add", "-t", "Hazardous", "--harm", "x", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W", "W3"], None).status.success());
assert!(String::from_utf8_lossy(&run(&["hazard", "list"], None).stdout).contains("SIL3"));
assert!(run(&["safety", "calibrate", "--set", "C_C/F_B/P_B=W3:4,W2:3,W1:2"], None).status.success());
assert!(String::from_utf8_lossy(&run(&["hazard", "list"], None).stdout).contains("SIL4"), "calibration override must change the derived SIL");
}