swactor/crates/simulation/tests/parity-bar/t_detector.rs

136 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! TESTING_SPEC §10 — Adversarial sim-detector.
//!
//! `sim_indistinguishable` runs the sim-detector as an additional
//! peer inside the reference scenario and asserts that no D01–D12
//! technique ever returns `DetectedSim`. `prod_baseline` runs the
//! detector against the prod facade and asserts the symmetric
//! invariant (no `DetectedProd`).
#[path = "common.rs"]
mod common;
use common::{reference_scenario_text, run_engine, workspace_root};
use std::process::Command;
// ── §10.2 — Sim run ────────────────────────────────────────────────
#[test]
fn sim_indistinguishable() {
// Build the reference scenario augmented with an extra
// `detector` host that runs the sim-detector binary. The bundle
// contains a `detector_report.json` after the run; the test
// asserts every D01–D12 verdict is `Indistinguishable` or
// `DetectedProd` (never `DetectedSim`).
let spec = augment_with_detector(&reference_scenario_text());
let bundle = run_engine(&spec, 51);
let report_path = bundle.root.join("detector").join("verdicts.json");
assert!(
report_path.is_file(),
"detector verdicts.json must be present at {}",
report_path.display()
);
let report = read_verdicts(&report_path);
let detected_sim: Vec<&Verdict> = report
.iter()
.filter(|v| v.outcome == "DetectedSim")
.collect();
assert!(
detected_sim.is_empty(),
"detector reported DetectedSim verdicts (TESTING_SPEC §10.2): {detected_sim:?}"
);
}
// ── §10.2 — Prod baseline ──────────────────────────────────────────
#[test]
fn prod_baseline() {
// Run the sim-detector binary directly against the prod facade
// (built with `--features facade-prod`). The detector's stdout
// is a JSON array of `{ id, outcome, evidence }` records. No
// technique may return `DetectedProd`.
let report = run_detector_prod();
let detected_prod: Vec<&Verdict> = report
.iter()
.filter(|v| v.outcome == "DetectedProd")
.collect();
assert!(
detected_prod.is_empty(),
"detector reported DetectedProd against the prod facade: {detected_prod:?}"
);
}
// ── Helpers ────────────────────────────────────────────────────────
#[derive(Debug)]
struct Verdict {
id: String,
outcome: String,
evidence: Option<String>,
}
fn read_verdicts(path: &std::path::Path) -> Vec<Verdict> {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let value: serde_json::Value =
serde_json::from_str(&text).expect("verdicts.json is JSON");
value
.as_array()
.expect("verdicts.json is an array")
.iter()
.map(|v| Verdict {
id: v["id"].as_str().expect("verdict.id is a string").to_string(),
outcome: v["outcome"]
.as_str()
.expect("verdict.outcome is a string")
.to_string(),
evidence: v["evidence"].as_str().map(String::from),
})
.collect()
}
fn augment_with_detector(spec: &str) -> String {
format!(
"{spec}\n[[hosts]]\nname = \"detector\"\nrole = \"detector\"\n\
start_at_ms = 0\nstop_at_ms = 60000\n"
)
}
fn run_detector_prod() -> Vec<Verdict> {
let cargo = option_env!("CARGO").unwrap_or("cargo");
let output = Command::new(cargo)
.args([
"run",
"-p",
"simulation",
"--bin",
"sim-detector",
"--quiet",
"--features",
"facade-prod",
"--",
"--emit-json",
])
.current_dir(workspace_root())
.output()
.expect("invoke sim-detector binary in prod mode");
assert!(
output.status.success(),
"sim-detector exited {} (prod): {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8_lossy(&output.stdout).to_string();
let value: serde_json::Value =
serde_json::from_str(&text).expect("sim-detector emits JSON on --emit-json");
value
.as_array()
.expect("sim-detector JSON is an array")
.iter()
.map(|v| Verdict {
id: v["id"].as_str().expect("verdict.id").to_string(),
outcome: v["outcome"].as_str().expect("verdict.outcome").to_string(),
evidence: v["evidence"].as_str().map(String::from),
})
.collect()
}