mod common;
use common::{stderr, stdout, Sandbox};
fn edit_raw(s: &Sandbox, f: impl FnOnce(&mut serde_json::Value)) {
let path = s.path();
let body = std::fs::read_to_string(&path).expect("read project.req");
let mut v: serde_json::Value = serde_json::from_str(&body).expect("parse project.req");
f(&mut v);
std::fs::write(&path, serde_json::to_string_pretty(&v).unwrap()).expect("write project.req");
}
fn add_req(s: &Sandbox, title: &str) {
let out = s.run(&[
"add",
"--title",
title,
"--statement",
"The system shall carry this requirement for the forward-compat fixture.",
"--rationale",
"Forward-compatibility test fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
assert!(out.status.success(), "add failed: {}", stderr(&out));
}
#[test]
fn req_0140_unknown_requirement_field_round_trips() {
let s = Sandbox::new();
s.init("p");
add_req(&s, "Has an unknown field from a future version");
edit_raw(&s, |v| {
v["requirements"]["REQ-0001"]["future_field"] =
serde_json::json!({"shape": "unknown", "n": 42});
});
let repair = s.run(&["repair", "--confirm-direct-edit"]);
assert!(
repair.status.success(),
"repair failed: {}",
stderr(&repair)
);
add_req(&s, "A second requirement triggering another save");
let body = std::fs::read_to_string(s.path()).unwrap();
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(
v["requirements"]["REQ-0001"]["future_field"]["n"], 42,
"unknown field must round-trip; got:\n{}",
body
);
}
#[test]
fn req_0141_save_refused_when_file_schema_rev_is_newer() {
let s = Sandbox::new();
s.init("p");
add_req(&s, "Baseline requirement before the rev bump");
edit_raw(&s, |v| {
v["_schema_rev"] = serde_json::json!(9999);
});
let before = std::fs::read_to_string(s.path()).unwrap();
let out = s.run(&[
"add",
"--title",
"This add must be refused",
"--statement",
"The system shall never persist this requirement.",
"--rationale",
"Guard fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
assert!(
!out.status.success(),
"add over a newer-schema file must fail; stdout={}",
stdout(&out)
);
let msg = stderr(&out).to_lowercase();
assert!(
msg.contains("newer") && (msg.contains("schema rev") || msg.contains("upgrade")),
"error should explain the schema-rev mismatch, got: {}",
stderr(&out)
);
let after = std::fs::read_to_string(s.path()).unwrap();
assert_eq!(before, after, "the file must be left untouched on refusal");
}
#[test]
fn req_0141_missing_schema_rev_is_treated_as_zero() {
let s = Sandbox::new();
s.init("p");
add_req(&s, "First requirement");
edit_raw(&s, |v| {
if let Some(obj) = v.as_object_mut() {
obj.remove("_schema_rev");
}
});
let repair = s.run(&["repair", "--confirm-direct-edit"]);
assert!(
repair.status.success(),
"repair failed: {}",
stderr(&repair)
);
add_req(&s, "Second requirement after the strip");
let body = std::fs::read_to_string(s.path()).unwrap();
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(
v.get("_schema_rev")
.and_then(serde_json::Value::as_u64)
.is_some(),
"save should re-stamp _schema_rev; got:\n{}",
body
);
}