joulecontract: the published negative vectors were never actually run #94
1 changed files with 139 additions and 0 deletions
|
|
@ -416,3 +416,142 @@ mod vector_pack_tests {
|
|||
assert_eq!(statement.subject.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod negative_vector_replay {
|
||||
use super::fixtures::appendix_a_sealed;
|
||||
use joulecontract_receipt::{validate_sealed, Receipt, SealedContext};
|
||||
|
||||
/// Every published `negative-*` vector is replayed through the real
|
||||
/// verifier and must reproduce its recorded verdict exactly.
|
||||
///
|
||||
/// # What was missing
|
||||
///
|
||||
/// The pack already had two tests: one checking every file's content
|
||||
/// address against `MANIFEST.json`, and one verifying the positive DSSE
|
||||
/// envelope. Neither *ran* a negative vector. They were published,
|
||||
/// digest-pinned, and never executed — so a verifier that stopped demoting
|
||||
/// would have kept every test green while the eleven vectors written to
|
||||
/// catch exactly that sat on disk unread.
|
||||
///
|
||||
/// # Why equality, not just "it demoted"
|
||||
///
|
||||
/// These are degradation vectors, not accept/reject ones. Each records the
|
||||
/// composed verdict a conformant verifier must produce: `metered_unattested`
|
||||
/// demotes Energy to `ModelBased`, `generator_unattested` caps Governance at
|
||||
/// `declared`, and so on. Asserting merely that *something* was demoted
|
||||
/// would pass a verifier that demoted the wrong axis, or by the wrong
|
||||
/// amount, or that refused the receipt outright — and refusing is not the
|
||||
/// specified behaviour either. The whole content of these vectors is the
|
||||
/// **exact** resulting claim, so the test compares all four recorded
|
||||
/// fields: the demotion list, the maximum profile, the rendered §15 claim
|
||||
/// string, and whether the claimed profile is permitted.
|
||||
#[test]
|
||||
fn every_negative_vector_reproduces_its_recorded_verdict() {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../conformance/v1");
|
||||
// Assert, not skip — matching `vector_pack_tests`. An early `return` here
|
||||
// would fire *before* the `replayed >= 11` guard below and let a missing
|
||||
// pack pass vacuously, which is precisely the failure that guard exists to
|
||||
// catch. The crate is not published, so it only runs in-repo, where the
|
||||
// pack is always present.
|
||||
assert!(
|
||||
dir.exists(),
|
||||
"conformance pack missing at {} — it is what this test replays",
|
||||
dir.display()
|
||||
);
|
||||
|
||||
// Rebuilt exactly as `gen_vectors` builds it: the negative cases mutate
|
||||
// the receipt and share this one context.
|
||||
let (contract, _r, bundle, log, interval) = appendix_a_sealed();
|
||||
let ctx = SealedContext {
|
||||
bundle: &bundle,
|
||||
key_log: &log,
|
||||
interval: &interval,
|
||||
transition_end_at: 150,
|
||||
max_anchor_latency_secs: 3600,
|
||||
open_breach_unamended: false,
|
||||
};
|
||||
|
||||
let mut replayed = 0usize;
|
||||
let mut entries: Vec<_> = std::fs::read_dir(&dir)
|
||||
.expect("read conformance dir")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| n.starts_with("negative-") && n.ends_with(".json"))
|
||||
.collect();
|
||||
entries.sort();
|
||||
|
||||
for name in &entries {
|
||||
let body = std::fs::read(dir.join(name)).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let v: serde_json::Value = serde_json::from_slice(&body)
|
||||
.unwrap_or_else(|e| panic!("{name}: parse: {e}"));
|
||||
|
||||
// The context the vector was generated under must be the context we
|
||||
// are replaying under, or the comparison is between two different
|
||||
// questions. Checked rather than assumed.
|
||||
let sc = &v["sealed_context"];
|
||||
assert_eq!(sc["transition_end_at"], 150, "{name}: context drifted");
|
||||
assert_eq!(sc["max_anchor_latency_secs"], 3600, "{name}: context drifted");
|
||||
assert_eq!(sc["open_breach_unamended"], false, "{name}: context drifted");
|
||||
|
||||
let receipt: Receipt = serde_json::from_value(v["receipt"].clone())
|
||||
.unwrap_or_else(|e| panic!("{name}: receipt: {e}"));
|
||||
|
||||
// A negative vector is demoted, never malformed: the receipt must
|
||||
// still validate structurally. A verifier that rejected it outright
|
||||
// would be wrong in the other direction.
|
||||
let verdict = validate_sealed(&contract, &receipt, &ctx)
|
||||
.unwrap_or_else(|e| panic!("{name}: must demote, not refuse: {e}"));
|
||||
|
||||
let expected = &v["expected_composed"];
|
||||
assert_eq!(
|
||||
serde_json::to_value(&verdict.demotions).unwrap(),
|
||||
expected["demotions"],
|
||||
"{name}: demotion list"
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&verdict.max_profile).unwrap(),
|
||||
expected["max_profile"],
|
||||
"{name}: maximum permitted profile"
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::Value::String(verdict.claim()),
|
||||
expected["claim"],
|
||||
"{name}: rendered claim"
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::Value::Bool(verdict.permits(&receipt.claimed_profile)),
|
||||
expected["permits_claimed_profile"],
|
||||
"{name}: whether the claimed profile is permitted"
|
||||
);
|
||||
|
||||
// What makes a vector negative is that the verifier *flagged*
|
||||
// something — not that it capped the profile. Two of the published
|
||||
// eleven deliberately still permit the claimed profile:
|
||||
// `recourse_undeclared` and `static_only_frame` record a
|
||||
// **disclosure** demotion rather than a cap. `static_only` is
|
||||
// prohibited only at JR-4 and these vectors claim JR-3, so the
|
||||
// honest verdict is "permitted, and here is what you are trusting".
|
||||
//
|
||||
// So the invariant is the demotion list, not the permission bit.
|
||||
// Asserting `!permits` here would have been wrong, and would have
|
||||
// pressured those two vectors toward a stricter reading than the
|
||||
// spec takes.
|
||||
assert!(
|
||||
!verdict.demotions.is_empty(),
|
||||
"{name}: a negative vector that produces no demotion is not \
|
||||
testing anything"
|
||||
);
|
||||
|
||||
replayed += 1;
|
||||
}
|
||||
|
||||
// Anti-vacuity. A wrong path, a renamed prefix or an empty glob would
|
||||
// otherwise let this test pass having checked nothing — the same shape
|
||||
// as the gap it was written to close.
|
||||
assert!(
|
||||
replayed >= 11,
|
||||
"expected at least the 11 published negative vectors, replayed {replayed}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue