swactor/crates/simulation/tests/cross_arch_parity.rs
Zachery Aaron Shores-Chmielewski 92d62daa33 feat(sim): add stage host kind and relay scenarios
Adds the pipeline-stage host kind wrapping the production stage-supervisor lifecycle
(Cold/Registering/Running/Halted per RELAY_SPEC), N3 relay calibration scenarios
(own-relay and canary-relay, real-worker and stub), relay invariant and
scenario-validation tests, and an adversarial judge test suite hunting spec/code gaps.
Expands SIM_SPEC to a three-layer failure decomposition.


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-05-25 14:33:35 +04:00

126 lines
5.1 KiB
Rust

//! SIM_SPEC §7.6 cross-architecture parity test.
//!
//! Runs the reference scenario through the full pipeline (scenario
//! loader → engine → network → bundle writer) using the deterministic
//! `parity_stub` host kind, then compares the SHA-256 of the bundle's
//! `events.ndjson` against the constant `EXPECTED_EVENTS_NDJSON_SHA256`
//! below.
//!
//! A mismatch is either:
//! - a deliberate spec amendment (in which case the commit that
//! changes the simulator's emission updates this constant and
//! explains why), or
//! - a determinism bug. The §7 contract says the bundle is
//! byte-identical across runs *and* architectures for the same
//! `seed`; this test gates against both.
//!
//! The test also runs the scenario twice in-process and asserts the
//! two digests match — that catches per-run drift even before the
//! constant gets updated.
use std::fs;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use tempfile::TempDir;
use simulation::bundle_file::FileBundleWriter;
use simulation::engine::Engine;
use simulation::network::Network;
use simulation::parity_host::{ParityStubHost, ParityStubKindValidator};
use simulation::scenario::{HostKindRegistry, Scenario, load_from_path};
/// SHA-256 of `events.ndjson` for the reference scenario at
/// `scenarios/parity/reference.toml`. Checked in; updated only as
/// part of a deliberate spec amendment.
// Updated when the parity reference scenario gained a relayed route
// (RELAY_SPEC §13 — "the cross-architecture parity test extended to
// cover relay-mediated routes"). The alpha↔charlie pair now routes
// through relay `R`, so the bundle stream contains `relay_enqueue` /
// `relay_dequeue` records and the alpha↔charlie arrival times shift
// to reflect the relay's ingress + egress serialization.
//
// Previous value, from iteration 10's DialOutcome correction:
// a76b557d3da7a5d0f393446231669b8f7b1945a251805bed33fa09fb27ab3db0
const EXPECTED_EVENTS_NDJSON_SHA256: &str =
"c7ee2a328c796f482b6c62694fc27936aa58a959b17f59fedd14a3f8c20ad2f2";
fn registry() -> HostKindRegistry {
let mut r = HostKindRegistry::with_swim();
r.register(Box::new(ParityStubKindValidator));
r
}
fn load_reference() -> Scenario {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("scenarios/parity/reference.toml");
load_from_path(&path, &registry()).expect("reference scenario must validate")
}
fn run_and_hash(scenario: &Scenario, out_dir: PathBuf) -> String {
let writer = FileBundleWriter::new(&out_dir, scenario.clone());
let network = Network::new(scenario);
let mut engine = Engine::new(scenario, network, writer);
let peer_ids: Vec<String> = scenario.peers.iter().map(|p| p.id.clone()).collect();
for peer in &scenario.peers {
engine.install_host(Box::new(ParityStubHost::new(
peer.id.clone(),
peer_ids.clone(),
)));
}
engine.set_pop_budget(10_000);
let _ = engine.run();
let writer = engine.into_writer();
writer.finalize().expect("finalize bundle");
hex_sha256_of(&fs::read(out_dir.join("events.ndjson")).unwrap())
}
fn hex_sha256_of(bytes: &[u8]) -> String {
let mut h = Sha256::new();
h.update(bytes);
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
// ──────────────────────────────────────────────────────────────────────
// §7.6 cross-architecture parity
// ──────────────────────────────────────────────────────────────────────
#[test]
fn reference_scenario_events_ndjson_matches_checked_in_digest() {
let scen = load_reference();
let tmp = TempDir::new().unwrap();
let digest = run_and_hash(&scen, tmp.path().join("bundle"));
assert_eq!(
digest, EXPECTED_EVENTS_NDJSON_SHA256,
"\
events.ndjson digest changed.
If this was a deliberate spec amendment, update the constant
EXPECTED_EVENTS_NDJSON_SHA256 in this file to:
{digest}
…and explain why in the commit message.
Otherwise this is a determinism regression: the same scenario+seed
produced different bytes than the checked-in reference."
);
}
#[test]
fn reference_scenario_is_deterministic_across_runs() {
let scen = load_reference();
let tmp = TempDir::new().unwrap();
let d1 = run_and_hash(&scen, tmp.path().join("bundle_a"));
let d2 = run_and_hash(&scen, tmp.path().join("bundle_b"));
assert_eq!(d1, d2, "two runs of the same scenario must produce the same digest");
}
#[test]
fn reference_scenario_changing_seed_changes_the_digest() {
// Negative: tweak the seed and verify the digest is *not* equal to
// the reference. Confirms the digest is sensitive to scenario input
// (i.e. not a constant byte sequence by accident).
let mut scen = load_reference();
scen.seed ^= 0x1234_5678;
let tmp = TempDir::new().unwrap();
let d = run_and_hash(&scen, tmp.path().join("bundle"));
assert_ne!(d, EXPECTED_EVENTS_NDJSON_SHA256);
}