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",
]);
s.run(&[
"verification",
"plan",
"SR-0001",
"--plan",
"review logic and bench-test the stop",
]);
s.run(&[
"verification",
"analysis",
"SR-0001",
"--findings",
"stop logic reviewed",
"--result",
"pass",
]);
s.run(&[
"verification",
"test",
"SR-0001",
"--findings",
"bench-measured stop time",
"--result",
"pass",
]);
s.run(&[
"verification",
"conclude",
"SR-0001",
"--statement",
"meets the stop obligation",
]);
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, "conform"]).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(&["conform"]);
assert!(
out.status.success(),
"well-formed safety chain must conform: {}",
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"]);
let sr = |n: u32| format!("SR-{:04}", n);
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 n in [1u32, 2] {
run(&[
"sreq",
"update",
&sr(n),
"--status",
"approved",
"--reason",
"r",
]);
run(&[
"sreq",
"update",
&sr(n),
"--status",
"implemented",
"--reason",
"r",
]);
}
std::fs::create_dir_all(root.join("src")).unwrap();
let ghost = sr(99);
std::fs::write(
root.join("src/x.rs"),
format!("// {}: here\n// {}: ghost\nfn x() {{}}\n", sr(1), ghost),
)
.unwrap();
let cov = run(&["coverage", "--path", "."]);
let out = String::from_utf8_lossy(&cov.stdout);
assert!(
out.contains(&sr(2)),
"the unmarked SR should be an orphan:\n{}",
out
);
assert!(
out.contains(&ghost),
"the unknown SR id should be a ghost:\n{}",
out
);
assert!(
!out.contains("SR ORPHANS") || !out.contains(&format!("{}\n {}", sr(1), sr(1))),
"the marked SR 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-disclaimer", "--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-disclaimer", "--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"
);
}
#[test]
fn req_0137_broken_safety_case_fails_validate() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
s.run(&[
"hazard",
"add",
"-t",
"Hazardous mode",
"--harm",
"operator hurt",
"-C",
"C_C",
"-F",
"F_B",
"-P",
"P_B",
"-W",
"W3",
]);
s.run(&["sf", "add", "-t", "Interlock", "--mitigates", "HAZ-0001"]);
assert!(
s.run(&["conform"]).status.success(),
"baseline chain must be clean"
);
assert!(
s.run(&[
"sf",
"update",
"SF-0001",
"--status",
"obsolete",
"--reason",
"retired without replacement"
])
.status
.success(),
"obsoleting the SF should itself succeed"
);
let broken = s.run(&["conform"]);
assert!(
!broken.status.success(),
"a broken safety case must fail conform with a non-zero exit"
);
let out = stdout(&broken) + &stderr(&broken);
assert!(
out.contains("REQ-V-0027"),
"conform must flag the mitigated-hazard-without-live-SF rule:\n{}",
out
);
}
#[test]
fn req_0011_safety_mutation_records_reasoned_append_only_history() {
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 reasons = ["reviewed at design gate", "implementation landed on main"];
s.run(&[
"sreq", "update", "SR-0001", "--status", "approved", "--reason", reasons[0],
]);
s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
"implemented",
"--reason",
reasons[1],
]);
let shown = stdout(&s.run(&["sreq", "show", "SR-0001", "--json"]));
let v: serde_json::Value = serde_json::from_str(&shown).expect("json");
let hist = v["history"].as_array().expect("history array present");
assert!(
hist.len() >= 3,
"history must accumulate, got {}:\n{}",
hist.len(),
shown
);
let recorded: Vec<String> = hist
.iter()
.filter_map(|e| e["reason"].as_str().map(str::to_string))
.collect();
assert!(
recorded.iter().any(|r| r == reasons[0]),
"first reason must be recorded: {:?}",
recorded
);
assert!(
recorded.iter().any(|r| r == reasons[1]),
"second reason must be recorded: {:?}",
recorded
);
assert!(
hist.iter()
.all(|e| e["action"].is_string() && e["actor"].is_string()),
"every history entry must carry an actor and action:\n{}",
shown
);
}
#[test]
fn req_0144_stale_disclaimer_version_blocks_safety_features() {
use std::process::Command;
let dir = tempfile::Builder::new()
.prefix("req-0144-")
.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")
.env_remove("REQ_ACTOR_KIND")
.output()
.expect("run req")
};
let hazard = [
"hazard", "add", "-t", "H", "--harm", "x", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W",
"W3",
];
assert!(run(&["init", "-n", "p"]).status.success());
std::fs::write(
root.join("req-safety-acceptance.json"),
r#"{"accepted_by":"Old","at":"2026-01-01T00:00:00Z","tool_version":"test","disclaimer_version":"1"}"#,
)
.unwrap();
let blocked = run(&hazard);
assert!(
!blocked.status.success(),
"an acceptance for an older disclaimer version must not enable safety features"
);
let msg = format!(
"{}{}",
String::from_utf8_lossy(&blocked.stderr),
String::from_utf8_lossy(&blocked.stdout)
);
assert!(
msg.to_lowercase().contains("accept"),
"the block should tell the user to re-accept: {msg}"
);
std::fs::write(
root.join("req-safety-acceptance.json"),
r#"{"accepted_by":"New","at":"2026-01-01T00:00:00Z","tool_version":"test","disclaimer_version":"2"}"#,
)
.unwrap();
let ok = run(&hazard);
assert!(
ok.status.success(),
"current-version acceptance must enable safety features: {}",
String::from_utf8_lossy(&ok.stderr)
);
}
#[test]
fn req_0145_safety_verification_needs_human_confirmation() {
use std::process::Command;
let dir = tempfile::Builder::new()
.prefix("req-0145-")
.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());
std::fs::write(
root.join("req-safety-acceptance.json"),
r#"{"accepted_by":"H","at":"2026-01-01T00:00:00Z","tool_version":"t","disclaimer_version":"2"}"#,
)
.unwrap();
run(
&[
"hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B",
"-W", "W3",
],
None,
);
run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"], None);
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",
],
None,
);
run(
&[
"sreq", "update", "SR-0001", "--status", "approved", "--reason", "r",
],
None,
);
run(
&[
"sreq",
"update",
"SR-0001",
"--status",
"implemented",
"--reason",
"r",
],
None,
);
run(
&[
"sreq",
"verify",
"SR-0001",
"--by",
"automated",
"--notes",
"bench",
],
None,
);
run(
&["verification", "plan", "SR-0001", "--plan", "review+bench"],
None,
);
run(
&[
"verification",
"analysis",
"SR-0001",
"--findings",
"reviewed",
"--result",
"pass",
],
None,
);
run(
&[
"verification",
"test",
"SR-0001",
"--findings",
"bench",
"--result",
"pass",
],
None,
);
assert!(run(
&[
"verification",
"conclude",
"SR-0001",
"--statement",
"meets the obligation",
"--promote"
],
None
)
.status
.success());
let v1 = run(&["conform"], None);
let v1out = format!(
"{}{}",
String::from_utf8_lossy(&v1.stdout),
String::from_utf8_lossy(&v1.stderr)
);
assert!(
v1.status.success(),
"awaiting-cosign must be a non-blocking advisory, not a hard error: {v1out}"
);
assert!(
v1out.contains("REQ-V-0038"),
"REQ-V-0038 advisory must name the awaiting safety requirement: {v1out}"
);
assert!(
String::from_utf8_lossy(&run(&["sreq", "show", "SR-0001"], None).stdout)
.contains("awaiting human co-sign"),
"sreq show must surface the awaiting state"
);
let by_agent = run(&["verification", "confirm", "SR-0001"], Some("agent"));
assert!(
!by_agent.status.success(),
"an agent must not be able to confirm a safety verification"
);
assert!(
run(&["verification", "confirm", "SR-0001"], Some("human"))
.status
.success(),
"a human confirmation must succeed"
);
let v2 = run(&["conform"], None);
assert!(
v2.status.success(),
"after human confirmation the project conforms clean: {}",
String::from_utf8_lossy(&v2.stderr)
);
}
#[test]
fn req_0146_trace_from_sr_shows_chain_and_dossier() {
use std::process::Command;
let dir = tempfile::Builder::new()
.prefix("req-0146-")
.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());
std::fs::write(
root.join("req-safety-acceptance.json"),
r#"{"accepted_by":"H","at":"2026-01-01T00:00:00Z","tool_version":"t","disclaimer_version":"2"}"#,
)
.unwrap();
run(
&[
"hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B",
"-W", "W3",
],
None,
);
run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"], None);
run(
&[
"sreq",
"add",
"-t",
"Stop the blade",
"-s",
"The system shall stop the blade on demand.",
"-r",
"operator safety",
"-a",
"blade stops within 200ms",
"--realizes",
"SF-0001",
],
None,
);
run(
&[
"sreq", "update", "SR-0001", "--status", "approved", "--reason", "r",
],
None,
);
run(
&[
"sreq",
"update",
"SR-0001",
"--status",
"implemented",
"--reason",
"r",
],
None,
);
run(
&[
"sreq",
"verify",
"SR-0001",
"--by",
"automated",
"--notes",
"bench",
],
None,
);
run(
&["verification", "plan", "SR-0001", "--plan", "review+bench"],
None,
);
run(
&[
"verification",
"analysis",
"SR-0001",
"--findings",
"reviewed",
"--result",
"pass",
],
None,
);
run(
&[
"verification",
"test",
"SR-0001",
"--findings",
"bench",
"--result",
"pass",
],
None,
);
run(
&[
"verification",
"conclude",
"SR-0001",
"--statement",
"meets the obligation",
"--promote",
],
None,
);
run(&["verification", "confirm", "SR-0001"], Some("human"));
let human = String::from_utf8_lossy(&run(&["trace", "SR-0001"], None).stdout).to_string();
assert!(
human.contains("HAZ-0001"),
"trace from an SR must resolve upward to the mitigated hazard:\n{human}"
);
assert!(
human.contains("dossier: verdict pass"),
"trace must inline the verification dossier verdict:\n{human}"
);
assert!(
human.contains("human-confirmed"),
"trace must show the human confirmation:\n{human}"
);
let j = String::from_utf8_lossy(&run(&["trace", "SR-0001", "--json"], None).stdout).to_string();
let v: serde_json::Value = serde_json::from_str(&j).expect("trace --json parses");
let val = &v["chain"][0]["safety_requirements"][0]["verification"];
assert!(
val.get("verdict").is_some() && val.get("human_confirmation").is_some(),
"--json chain must include the SR's verification dossier:\n{j}"
);
}
fn setup_marked_confirmed_sr(root: &std::path::Path) {
use std::process::Command;
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")
};
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(
root.join("src/safety_impl.rs"),
"// SR-0001: the interlock implementation\npub fn interlock() {}\n",
)
.unwrap();
std::fs::write(
root.join("notes.md"),
"Design notes: SR-0001 keeps the operator safe.\n",
)
.unwrap();
assert!(run(&["init", "-n", "p"], None).status.success());
std::fs::write(
root.join("req-safety-acceptance.json"),
r#"{"accepted_by":"H","at":"2026-01-01T00:00:00Z","tool_version":"t","disclaimer_version":"2"}"#,
)
.unwrap();
run(
&[
"hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B",
"-W", "W3",
],
None,
);
run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"], None);
run(
&[
"sreq",
"add",
"-t",
"Interlock",
"-s",
"The system shall engage the interlock on demand.",
"-r",
"operator safety",
"-a",
"interlock engages",
"--realizes",
"SF-0001",
],
None,
);
run(
&[
"sreq", "update", "SR-0001", "--status", "approved", "--reason", "r",
],
None,
);
run(
&[
"sreq",
"update",
"SR-0001",
"--status",
"implemented",
"--reason",
"r",
],
None,
);
run(
&[
"sreq",
"verify",
"SR-0001",
"--by",
"automated",
"--notes",
"bench",
],
None,
);
run(&["verification", "plan", "SR-0001", "--plan", "p"], None);
run(
&[
"verification",
"analysis",
"SR-0001",
"--findings",
"ok",
"--result",
"pass",
],
None,
);
run(
&[
"verification",
"test",
"SR-0001",
"--findings",
"ok",
"--result",
"pass",
],
None,
);
run(
&[
"verification",
"conclude",
"SR-0001",
"--statement",
"meets",
"--promote",
],
None,
);
run(&["verification", "confirm", "SR-0001"], Some("human"));
}
fn req_in(root: &std::path::Path, args: &[&str]) -> std::process::Output {
std::process::Command::new(env!("CARGO_BIN_EXE_req"))
.args(args)
.current_dir(root)
.env_remove("REQ_FILE")
.env_remove("REQ_ACTOR_KIND")
.output()
.expect("run req")
}
#[test]
fn req_0149_staleness_scopes_to_comment_markers_not_prose() {
let dir = tempfile::Builder::new()
.prefix("req-0149-")
.tempdir()
.unwrap();
let root = dir.path();
setup_marked_confirmed_sr(root);
let shown = String::from_utf8_lossy(
&req_in(root, &["verification", "show", "SR-0001", "--json"]).stdout,
)
.to_string();
let v: serde_json::Value = serde_json::from_str(&shown).expect("verification show --json");
let linked: Vec<String> = v["verification"]["linked_files"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
assert!(
linked.iter().any(|f| f.contains("safety_impl.rs")),
"the code-comment marker file must be a dependency: {linked:?}"
);
assert!(
!linked.iter().any(|f| f.contains("notes.md")),
"a prose-only mention must NOT be a dependency: {linked:?}"
);
std::fs::write(root.join("notes.md"), "Design notes: rewritten prose.\n").unwrap();
assert!(
req_in(root, &["conform"]).status.success(),
"editing prose must not invalidate the safety requirement"
);
}
#[test]
fn req_0148_stale_safety_requirement_is_a_validate_error() {
let dir = tempfile::Builder::new()
.prefix("req-0148-")
.tempdir()
.unwrap();
let root = dir.path();
setup_marked_confirmed_sr(root);
assert!(
req_in(root, &["conform"]).status.success(),
"a freshly verified + confirmed SR should pass"
);
std::fs::write(
root.join("src/safety_impl.rs"),
"// SR-0001: the interlock implementation\npub fn interlock() { /* changed */ }\n",
)
.unwrap();
let out = req_in(root, &["conform"]);
assert!(
!out.status.success(),
"a stale safety requirement must fail conform"
);
let msg = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
msg.contains("REQ-V-0035"),
"stale SR must be flagged REQ-V-0035:\n{msg}"
);
}
#[test]
fn req_0158_safety_requirement_obeys_status_ladder() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let _ = s.run(&[
"hazard",
"add",
"-t",
"H",
"--harm",
"someone is hurt",
"-C",
"C_C",
"-F",
"F_B",
"-P",
"P_B",
"-W",
"W3",
]);
let _ = s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"R",
"-s",
"The system shall stop.",
"-r",
"because",
"-a",
"stops",
"--realizes",
"SF-0001",
]);
let up = s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
"verified",
"--force",
"--reason",
"seed verified state for the ladder test",
]);
assert!(up.status.success(), "seed verify: {}", stderr(&up));
let demote = s.run(&["sreq", "update", "SR-0001", "--status", "draft"]);
assert!(
!demote.status.success(),
"SR demotion without --force should be rejected"
);
assert!(
stderr(&demote).contains("irregular transition"),
"expected irregular-transition error, got: {}",
stderr(&demote)
);
let forced = s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
"draft",
"--force",
"--reason",
"correcting a bad promotion record",
]);
assert!(
forced.status.success(),
"forced demote: {}",
stderr(&forced)
);
}
fn verified_sil2_chain() -> Sandbox {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let _ = s.run(&[
"hazard",
"add",
"-t",
"Hazard A",
"--harm",
"someone is hurt",
"-C",
"C_C",
"-F",
"F_B",
"-P",
"P_B",
"-W",
"W2", ]);
let _ = s.run(&["sf", "add", "-t", "Function", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"Requirement",
"-s",
"The system shall stop within 200 ms.",
"-r",
"bounds exposure",
"-a",
"stops",
"--realizes",
"SF-0001",
]);
let _ = s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
"proposed",
"--reason",
"advance for verification",
]);
let _ = s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
"approved",
"--reason",
"advance for verification",
]);
let _ = s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
"implemented",
"--reason",
"advance for verification",
]);
let _ = s.run(&[
"verification",
"plan",
"SR-0001",
"--plan",
"analysis + testing of the stop path",
]);
let _ = s.run(&[
"verification",
"analysis",
"SR-0001",
"--result",
"pass",
"--findings",
"reviewed the stop path",
]);
let _ = s.run(&[
"verification",
"test",
"SR-0001",
"--result",
"pass",
"--findings",
"bench test passes",
]);
let _ = s.run(&[
"verification",
"conclude",
"SR-0001",
"--statement",
"SR-0001 met: stop path verified.",
"--promote",
]);
let _ = s.run(&[
"verification",
"confirm",
"SR-0001",
"--note",
"reviewed and accepted",
]);
s
}
#[test]
fn req_0154_evidence_snapshots_sil_at_verification() {
let s = verified_sil2_chain();
let show = stdout(&s.run(&["sreq", "show", "SR-0001"]));
assert!(
show.contains("SIL2 (verified at)"),
"sreq show should display the verification-time SIL:\n{}",
show
);
}
#[test]
fn req_0155_escalation_flags_evidence_below_current_sil() {
let s = verified_sil2_chain();
let _ = s.run(&[
"hazard",
"add",
"-t",
"Hazard B",
"--harm",
"worse harm",
"-C",
"C_C",
"-F",
"F_B",
"-P",
"P_B",
"-W",
"W3", ]);
let _ = s.run(&["sf", "mitigate", "SF-0001", "HAZ-0002"]);
let val = s.run(&["conform"]);
let body = format!("{}{}", stdout(&val), stderr(&val));
assert!(
body.contains("REQ-V-0036"),
"escalation should raise REQ-V-0036:\n{}",
body
);
let show = stdout(&s.run(&["sreq", "show", "SR-0001"]));
assert!(
show.contains("inherited SIL rose"),
"sreq show should flag the escalation:\n{}",
show
);
}
#[test]
fn req_0157_brief_surfaces_sil_escalated() {
let s = verified_sil2_chain();
let _ = s.run(&[
"hazard",
"add",
"-t",
"Hazard B",
"--harm",
"worse harm",
"-C",
"C_C",
"-F",
"F_B",
"-P",
"P_B",
"-W",
"W3",
]);
let _ = s.run(&["sf", "mitigate", "SF-0001", "HAZ-0002"]);
let brief = s.run(&["brief", "--json"]);
let v: serde_json::Value = serde_json::from_str(&stdout(&brief)).expect("brief json");
let esc = v["sil_escalated"].as_array().expect("sil_escalated array");
assert!(
esc.iter()
.any(|x| x.as_str().unwrap_or("").contains("SR-0001")),
"brief should surface escalated SR-0001: {}",
v["sil_escalated"]
);
}
#[test]
fn req_0160_trace_labels_verification_scope() {
let s = verified_sil2_chain();
let trace = stdout(&s.run(&["trace", "HAZ-0001"]));
assert!(
trace.contains("NOT a residual-risk verification"),
"trace must state its scope:\n{}",
trace
);
assert!(
!trace.contains("residual risk is acceptable"),
"trace must not imply residual risk is acceptable:\n{}",
trace
);
}
#[test]
fn req_0168_warns_when_author_verifies_own_safety_requirement() {
let s = verified_sil2_chain();
let val = s.run(&["conform"]);
let body = format!("{}{}", stdout(&val), stderr(&val));
assert!(
body.contains("REQ-V-0037"),
"same author+verifier should warn REQ-V-0037:\n{}",
body
);
}
use std::process::Command as PCommand;
fn git_sandbox_run(s: &Sandbox, args: &[&str]) -> std::process::Output {
let mut full: Vec<String> = vec!["--file".into(), s.path().to_str().unwrap().into()];
full.extend(args.iter().map(|a| a.to_string()));
PCommand::new(env!("CARGO_BIN_EXE_req"))
.current_dir(s.dir.path()) .args(&full)
.env_remove("REQ_FILE")
.output()
.expect("invoke req")
}
fn git(dir: &std::path::Path, args: &[&str]) {
let _ = PCommand::new("git").current_dir(dir).args(args).output();
}
fn walkthrough_chain() -> Sandbox {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let dir = s.dir.path();
git(dir, &["init", "-q", "-b", "main"]);
git(dir, &["config", "user.email", "t@example.com"]);
git(dir, &["config", "user.name", "Tester"]);
git(dir, &["add", "-A"]);
git(dir, &["commit", "-q", "-m", "init"]);
let _ = s.run(&[
"hazard", "add", "-t", "Hazard A", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B",
"-W", "W2",
]);
let _ = s.run(&["sf", "add", "-t", "Func", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"Req",
"-s",
"The system shall stop.",
"-r",
"bounds",
"-a",
"stops",
"--realizes",
"SF-0001",
]);
let _ = s.run(&[
"sreq",
"verify",
"SR-0001",
"--by",
"automated",
"--notes",
"bench pass",
]);
s
}
#[test]
fn req_0173_walkthrough_acknowledge_refuses_agent() {
let s = walkthrough_chain();
let out = PCommand::new(env!("CARGO_BIN_EXE_req"))
.current_dir(s.dir.path())
.args([
"--file",
s.path().to_str().unwrap(),
"safety",
"acknowledge",
"SR-0001",
])
.env_remove("REQ_FILE")
.env("REQ_ACTOR_KIND", "agent")
.output()
.expect("invoke");
assert!(!out.status.success(), "agent ack must be refused");
assert!(stderr(&out).contains("must be made by a human"));
}
#[test]
fn req_0174_walkthrough_refuses_incomplete_chain() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let dir = s.dir.path();
git(dir, &["init", "-q", "-b", "main"]);
git(dir, &["config", "user.email", "t@example.com"]);
git(dir, &["config", "user.name", "Tester"]);
git(dir, &["add", "-A"]);
git(dir, &["commit", "-q", "-m", "init"]);
let _ = s.run(&[
"hazard", "add", "-t", "Hazard A", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B",
"-W", "W2",
]);
let _ = s.run(&["sf", "add", "-t", "Func", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"Req",
"-s",
"The system shall stop.",
"-r",
"bounds",
"-a",
"stops",
"--realizes",
"SF-0001",
]);
let out = git_sandbox_run(&s, &["safety", "acknowledge", "SR-0001"]);
assert!(
!out.status.success(),
"incomplete chain ack must be refused"
);
assert!(stderr(&out).contains("cannot be acknowledged"));
}
#[test]
fn req_0169_172_walkthrough_gate_and_acknowledge() {
let s = walkthrough_chain();
let render = git_sandbox_run(&s, &["safety", "walkthrough"]);
assert!(render.status.success());
let body = stdout(&render);
assert!(
body.contains("HAZ-0001") && body.contains("SF-0001") && body.contains("SR-0001"),
"chain shown:\n{}",
body
);
let before = git_sandbox_run(&s, &["safety", "walkthrough", "--gate"]);
assert!(!before.status.success(), "gate should fail before ack");
let ack = git_sandbox_run(
&s,
&["safety", "acknowledge", "SR-0001", "--note", "reviewed"],
);
assert!(ack.status.success(), "ack: {}", stderr(&ack));
let after = git_sandbox_run(&s, &["safety", "walkthrough", "--gate"]);
assert!(
after.status.success(),
"gate should pass after ack: {}",
stderr(&after)
);
}
#[test]
fn req_0198_walkthrough_shows_dossier() {
let s = walkthrough_chain();
let _ = s.run(&["verification", "plan", "SR-0001", "--plan", "review + test"]);
let _ = s.run(&[
"verification",
"analysis",
"SR-0001",
"--result",
"pass",
"--findings",
"code reviewed",
"--ref",
"src/lib.rs",
]);
let _ = s.run(&[
"verification",
"test",
"SR-0001",
"--result",
"pass",
"--findings",
"bench passed",
"--ref",
"tests/x.rs",
]);
let _ = s.run(&[
"verification",
"conclude",
"SR-0001",
"--statement",
"obligation met",
]);
let render = git_sandbox_run(&s, &["safety", "walkthrough", "SR-0001"]);
assert!(render.status.success());
let body = stdout(&render);
assert!(
body.contains("EVIDENCE") && body.contains("co-sign"),
"default view should show the dossier verdict + co-sign line:\n{}",
body
);
let full = git_sandbox_run(&s, &["safety", "walkthrough", "SR-0001", "--full"]);
assert!(full.status.success());
let fbody = stdout(&full);
assert!(
fbody.contains("analysis") && fbody.contains("testing") && fbody.contains("refs:"),
"full view should expand analysis/testing with refs:\n{}",
fbody
);
}
#[test]
fn req_0199_interactive_falls_back_without_tty() {
let s = walkthrough_chain();
let out = git_sandbox_run(&s, &["safety", "walkthrough", "SR-0001", "-i"]);
assert!(
out.status.success(),
"should not hang or fail: {}",
stderr(&out)
);
assert!(
stdout(&out).contains("SR-0001"),
"fallback should render the chain statically:\n{}",
stdout(&out)
);
}
#[test]
fn req_0187_conclude_leaves_sr_awaiting_then_confirm_promotes() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let _ = s.run(&[
"hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W",
"W2",
]);
let _ = s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"Stop the line",
"-s",
"The system shall stop the line on demand.",
"-r",
"operator safety",
"-a",
"stops within 200ms",
"--realizes",
"SF-0001",
]);
for st in ["proposed", "approved", "implemented"] {
let _ = s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
st,
"--reason",
"advance for verification",
]);
}
let _ = s.run(&[
"verification",
"plan",
"SR-0001",
"--plan",
"review + bench",
]);
let _ = s.run(&[
"verification",
"analysis",
"SR-0001",
"--result",
"pass",
"--findings",
"logic reviewed",
]);
let _ = s.run(&[
"verification",
"test",
"SR-0001",
"--result",
"pass",
"--findings",
"bench passes",
]);
let c = s.run(&[
"verification",
"conclude",
"SR-0001",
"--statement",
"stop obligation met",
"--promote",
]);
assert!(c.status.success(), "{}", stderr(&c));
assert!(
!stdout(&s.run(&["sreq", "list"])).contains("verified"),
"SR must not be Verified after agent conclude"
);
assert!(stdout(&s.run(&["sreq", "show", "SR-0001"])).contains("awaiting human co-sign"));
let by_agent = common::req(&[
"--file",
s.path().to_str().unwrap(),
"verification",
"confirm",
"SR-0001",
]);
let by_agent2 = {
use std::process::Command;
Command::new(env!("CARGO_BIN_EXE_req"))
.args([
"--file",
s.path().to_str().unwrap(),
"verification",
"confirm",
"SR-0001",
])
.env_remove("REQ_FILE")
.env("REQ_ACTOR_KIND", "agent")
.output()
.unwrap()
};
let _ = by_agent;
assert!(!by_agent2.status.success(), "agent must not confirm");
let conf = s.run(&["verification", "confirm", "SR-0001", "--note", "reviewed"]);
assert!(conf.status.success(), "{}", stderr(&conf));
assert!(stdout(&s.run(&["sreq", "show", "SR-0001"])).contains("verified"));
}
#[test]
fn req_0188_awaiting_cosign_is_advisory_not_error() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let _ = s.run(&[
"hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W",
"W2",
]);
let _ = s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"Stop the line",
"-s",
"The system shall stop the line on demand.",
"-r",
"operator safety",
"-a",
"stops within 200ms",
"--realizes",
"SF-0001",
]);
for st in ["proposed", "approved", "implemented"] {
let _ = s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
st,
"--reason",
"advance for verification",
]);
}
let _ = s.run(&[
"verification",
"plan",
"SR-0001",
"--plan",
"review + bench",
]);
let _ = s.run(&[
"verification",
"analysis",
"SR-0001",
"--result",
"pass",
"--findings",
"logic reviewed",
]);
let _ = s.run(&[
"verification",
"test",
"SR-0001",
"--result",
"pass",
"--findings",
"bench passes",
]);
let _ = s.run(&[
"verification",
"conclude",
"SR-0001",
"--statement",
"met",
"--promote",
]);
let v = s.run(&["conform"]);
assert!(
v.status.success(),
"awaiting must not block conform: {}",
stdout(&v)
);
let body = format!("{}{}", stdout(&v), stderr(&v));
assert!(
body.contains("REQ-V-0038"),
"advisory must name the awaiting SR: {}",
body
);
}
#[test]
fn req_0189_trace_incomplete_until_cosign() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let _ = s.run(&[
"hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W",
"W2",
]);
let _ = s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"Stop the line",
"-s",
"The system shall stop the line on demand.",
"-r",
"operator safety",
"-a",
"stops within 200ms",
"--realizes",
"SF-0001",
]);
for st in ["proposed", "approved", "implemented"] {
let _ = s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
st,
"--reason",
"advance for verification",
]);
}
let _ = s.run(&[
"verification",
"plan",
"SR-0001",
"--plan",
"review + bench",
]);
let _ = s.run(&[
"verification",
"analysis",
"SR-0001",
"--result",
"pass",
"--findings",
"logic reviewed",
]);
let _ = s.run(&[
"verification",
"test",
"SR-0001",
"--result",
"pass",
"--findings",
"bench passes",
]);
let _ = s.run(&[
"verification",
"conclude",
"SR-0001",
"--statement",
"met",
"--promote",
]);
let t1 = stdout(&s.run(&["trace", "HAZ-0001"]));
assert!(
t1.contains("incomplete"),
"trace must be incomplete pre-cosign: {}",
t1
);
assert!(
t1.contains("SR-0001 awaiting human co-sign"),
"blocker named: {}",
t1
);
let _ = s.run(&["verification", "confirm", "SR-0001"]);
let t2 = stdout(&s.run(&["trace", "HAZ-0001"]));
assert!(
t2.contains("linked and verified"),
"trace complete after cosign: {}",
t2
);
}
fn impact_chain() -> Sandbox {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let _ = s.run(&[
"hazard", "add", "-t", "HA", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W",
"W2",
]); let _ = s.run(&[
"hazard", "add", "-t", "HB", "--harm", "worse", "-C", "C_C", "-F", "F_B", "-P", "P_B",
"-W", "W3",
]); let _ = s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"R",
"-s",
"The system shall stop on demand.",
"-r",
"bounds",
"-a",
"stops",
"--realizes",
"SF-0001",
]);
s
}
#[test]
fn req_0156_impact_link_shows_before_after_without_mutating() {
let s = impact_chain();
let before = stdout(&s.run(&["sreq", "show", "SR-0001"]));
let out = s.run(&["impact", "--mitigate", "SF-0001=HAZ-0002", "--json"]);
assert!(out.status.success(), "{}", stderr(&out));
let v: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("impact json");
assert_eq!(v["applied"], false);
let changes = v["changes"].as_array().unwrap();
let sf = changes
.iter()
.find(|c| c["id"] == "SF-0001")
.expect("SF changed");
assert_eq!(sf["before"], "SIL2");
assert_eq!(sf["after"], "SIL3");
assert!(changes
.iter()
.any(|c| c["id"] == "SR-0001" && c["after"] == "SIL3"));
assert_eq!(before, stdout(&s.run(&["sreq", "show", "SR-0001"])));
}
#[test]
fn req_0156_impact_calibration_shows_before_after() {
let s = impact_chain();
let out = s.run(&[
"impact",
"--calibrate",
"C_C/F_B/P_B=W3:SIL1,W2:SIL1,W1:SIL1",
"--json",
]);
assert!(out.status.success(), "{}", stderr(&out));
let v: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("impact json");
let changes = v["changes"].as_array().unwrap();
assert!(
changes
.iter()
.any(|c| c["id"] == "HAZ-0002" && c["before"] == "SIL3" && c["after"] == "SIL1"),
"{}",
v["changes"]
);
}
#[test]
fn req_0156_impact_requires_a_proposed_edit() {
let s = impact_chain();
let out = s.run(&["impact"]);
assert!(!out.status.success(), "impact with no edit should error");
assert!(stderr(&out).contains("nothing to analyse"));
}
#[test]
fn req_0172_no_deadlock_walkthrough_acknowledge_under_stale_acceptance() {
let s = walkthrough_chain(); std::fs::write(
s.dir.path().join("req-safety-acceptance.json"),
r#"{"accepted_by":"H","at":"2026-01-01T00:00:00Z","tool_version":"old","disclaimer_version":"1"}"#,
)
.unwrap();
let wt = git_sandbox_run(&s, &["safety", "walkthrough"]);
assert!(
wt.status.success(),
"walkthrough under stale acceptance: {}",
stderr(&wt)
);
let ack = git_sandbox_run(
&s,
&["safety", "acknowledge", "SR-0001", "--note", "reviewed"],
);
assert!(
ack.status.success(),
"acknowledge under stale acceptance: {}",
stderr(&ack)
);
let hz = git_sandbox_run(
&s,
&[
"hazard", "add", "-t", "H2", "--harm", "x", "-C", "C_C", "-F", "F_B", "-P", "P_B",
"-W", "W2",
],
);
assert!(!hz.status.success(), "mutations must stay version-gated");
}
#[test]
fn req_0193_accept_disclaimer_not_gated_on_acknowledgement() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let _ = s.run(&[
"hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W",
"W2",
]);
let _ = s.run(&["sf", "add", "-t", "F", "--mitigates", "HAZ-0001"]);
let _ = s.run(&[
"sreq",
"add",
"-t",
"R",
"-s",
"The system shall stop on demand.",
"-r",
"bounds",
"-a",
"stops",
"--realizes",
"SF-0001",
]);
let out = s.run(&["safety", "accept-disclaimer", "--name", "Tom"]);
assert!(!out.status.success());
let err = stderr(&out);
assert!(
err.contains("interactive terminal"),
"should fail on TTY, not acks: {}",
err
);
assert!(
!err.to_lowercase().contains("acknowledge"),
"activation must not be gated on acknowledgement: {}",
err
);
assert!(
!s.run(&["safety", "accept", "--help"]).status.success(),
"bare `accept` should be gone in favour of accept-disclaimer"
);
}