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

173 lines
6.7 KiB
Rust
Raw Normal View History

2026-05-21 19:48:02 +00:00
//! TESTING_SPEC §3 — Replay isomorphism.
//!
//! The recording must carry every piece of state needed to drive a
//! replay; the replay must reproduce the original bundle byte for
//! byte, even after the sim-only `sim/` subtree has been stripped;
//! and a real prod-origin bundle must replay through the same code
//! path.
#[path = "common.rs"]
mod common;
use common::{
corpus_root, reference_scenario_text, run_engine, run_reference_scenario, sha256_tree,
};
// ── §3.1 — Self-replay ─────────────────────────────────────────────
#[test]
fn self_replay_identical() {
// Run → record → replay → recording. The two bundles must be
// byte-identical: any difference proves the original recording
// dropped state SPEC §7.1 demands it carry.
let primary = run_reference_scenario();
let replayed = replay_bundle(&primary.root);
assert_eq!(
sha256_tree(&primary.root),
sha256_tree(&replayed.root),
"self-replay must produce a byte-identical bundle (SPEC §7.1)"
);
}
// ── §3.2 — Mutation fidelity ──────────────────────────────────────
#[test]
fn mutations_preserved() {
// The reference scenario declares partition / heal / restart
// mutations at fixed `at_ms`. Each must appear in the bundle as
// a `Custom` event on the recording stream SPEC §4.1/§5.3
// designates, in declaration order, with virtual-time `wall_ms`
// matching the spec exactly.
let bundle = run_reference_scenario();
let mutations = collect_mutation_events(&bundle.root);
let expected: &[(&str, u64)] = &[
("partition", 20_000),
("heal", 35_000),
("restart", 45_000),
];
assert_eq!(
mutations.len(),
expected.len(),
"exactly {} mutation events expected, observed {} — {:?}",
expected.len(),
mutations.len(),
mutations
);
for ((kind, at_ms), (obs_kind, obs_at)) in expected.iter().zip(mutations.iter()) {
assert_eq!(obs_kind, kind, "mutation order/kind mismatch");
assert_eq!(
obs_at, at_ms,
"mutation {kind} should fire at wall_ms={at_ms}, observed {obs_at}"
);
}
}
// ── §3.3 — Recording self-sufficiency under prod shape ────────────
#[test]
fn replay_from_prod_shape_only() {
// Strip the `sim/` subtree (the sim-only fields) and replay
// from what's left. The replay must succeed and produce a
// bundle whose non-`sim/` files hash identically to the original.
let primary = run_reference_scenario();
let stripped = clone_without_sim(&primary.root);
let replayed = replay_bundle(&stripped);
let original_no_sim = sha256_tree_excluding_sim(&primary.root);
let replayed_no_sim = sha256_tree_excluding_sim(&replayed.root);
assert_eq!(
original_no_sim, replayed_no_sim,
"replay must reproduce non-sim/ files even when sim/ is absent (SPEC §7.1)"
);
}
// ── §3.4 — Prod-bundle replay (corpus-anchored) ───────────────────
#[test]
fn vastai_n3_replays() {
// The locked vastai-n3 corpus must replay through the same
// loader. The replay output is not required to be byte-identical
// to the corpus (calibration concern, out of scope per §15); it
// must merely complete and produce a structurally valid bundle.
let corpus = corpus_root();
let replayed = replay_bundle(&corpus);
// §7.3-style minimal structural check: every node in the corpus
// has a matching directory in the replayed bundle, each
// containing `boot.json`.
for node in ["orchestrator", "stage-0", "stage-1", "stage-2"] {
let node_dir = replayed.root.join(node);
assert!(
node_dir.is_dir(),
"replayed bundle missing node directory: {}",
node_dir.display()
);
assert!(
node_dir.join("boot.json").is_file(),
"replayed {node} missing boot.json"
);
}
}
// ── Helpers ────────────────────────────────────────────────────────
fn replay_bundle(bundle_path: &std::path::Path) -> simulation::Bundle {
// Re-run the engine in replay mode by appending a `[replay]`
// table pointing at `bundle_path` to the reference scenario text;
// `run_to_tempdir` recognises the table and routes the spec
// through `simulation::replay::load_replay_spec`.
let spec_text = reference_scenario_text();
let augmented = format!("{spec_text}\n[replay]\nbundle = {:?}\n", bundle_path.display());
run_engine(&augmented, 42)
}
fn collect_mutation_events(bundle_root: &std::path::Path) -> Vec<(String, u64)> {
// Mutations are written to the orchestrator's event stream as
// `Custom { user_kind: "partition" | "heal" | "restart", ... }`.
let orch_events = common::flatten_events(&bundle_root.join("orchestrator"));
orch_events
.into_iter()
.filter_map(|rec| {
let variant = rec.get("variant").and_then(|v| v.as_str())?;
if variant != "Custom" {
return None;
}
let kind = rec.get("user_kind").and_then(|v| v.as_str())?;
if !matches!(kind, "partition" | "heal" | "restart") {
return None;
}
let at = rec.get("wall_ms").and_then(|v| v.as_u64())?;
Some((kind.to_string(), at))
})
.collect()
}
fn clone_without_sim(bundle_root: &std::path::Path) -> std::path::PathBuf {
let dest = bundle_root.with_extension("stripped");
std::fs::create_dir_all(&dest)
.unwrap_or_else(|e| panic!("create_dir_all {}: {e}", dest.display()));
common::walk_files(bundle_root, &mut |path| {
let rel = path
.strip_prefix(bundle_root)
.expect("walk yields paths under bundle_root");
if rel.starts_with("sim") {
return;
}
let dst = dest.join(rel);
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)
.unwrap_or_else(|e| panic!("create_dir_all {}: {e}", parent.display()));
}
std::fs::copy(path, &dst)
.unwrap_or_else(|e| panic!("copy {} → {}: {e}", path.display(), dst.display()));
});
dest
}
fn sha256_tree_excluding_sim(root: &std::path::Path) -> std::collections::BTreeMap<std::path::PathBuf, String> {
sha256_tree(root)
.into_iter()
.filter(|(rel, _)| !rel.starts_with("sim"))
.collect()
}