swactor/crates/simulation/TESTING_SPEC.md

1094 lines
35 KiB
Markdown
Raw Normal View History

2026-05-21 19:48:02 +00:00
# Deterministic Testing Spec for the Simulator
> Companion to [`NORTH_STAR.md`](./NORTH_STAR.md), [`SPEC.md`](./SPEC.md),
> and [`OBSERVABILITY.md`](./OBSERVABILITY.md). Read those first.
>
> This document is the closed, enumerated contract of binary pass/fail
> checks the simulator must satisfy before it is considered built. It
> exists because the simulator's quality bar — observable equivalence
> with production, per NORTH_STAR — is statistical, and statistical
> bars are asymptotic by nature: there is always a metric just over
> the line. An asymptote cannot be the agent's done-criterion. The
> agent's done-criterion is "every check in §2–§12 of this document is
> green." Statistical parity (the calibration loop, SPEC §9) lives
> outside this spec and is gated by humans.
>
> The simulator is being built by a code agent operating without
> direct feedback. This spec, the three docs above, and the test
> suite it mandates are the only signals the agent has. Sections of
> this document must therefore be read as binding constraints, not
> guidelines.
## Table of contents
0. Reading instructions
1. Test corpus
2. Determinism oracle
3. Replay isomorphism
4. Facade integrity
5. Same-binary invariant
6. Schema floor coverage
7. Schema round-trip
8. Causality and time invariants
9. Equivariance
10. Adversarial sim-detector
11. Lifecycle observability
12. Test infrastructure integrity
13. Definition of done
14. Failure protocol
15. Out of scope
16. Glossary
---
## 0. Reading instructions
**Each section below names tests that must exist and pass.** The
checks are binary: zero tolerance, no thresholds, no "approximately."
Wherever a section calls a check binary, that is the literal
expectation — same SHA-256, identical byte ranges, identical record
counts, identical sorted symbol sets.
**The check list is closed.** New checks may be *added* (extensions
to §10 in particular are expected as new detection techniques are
discovered) but no check in this document may be removed, relaxed,
weakened, marked `#[ignore]`, gated behind an env var, or skipped
on any platform. Any change to this file or to the parity-bar test
directory requires a separate commit that is gated by §12.1.
**Tests are addressed to the code, not to themselves.** Per the
project conventions in `CLAUDE.md`, white-box / structural tests
("does this call this function") are not acceptable substitutes for
the checks listed here. Where a check is a scenario test, write the
scenario. Where it is a property, use property machinery. Where it
is an invariant on real output, assert against the real output.
**Where a check needs a fixture that does not yet exist, the agent
is to create the fixture inside the parity-bar tree, document its
provenance in `tests/parity-bar/fixtures/PROVENANCE.md`, and proceed.**
Fixtures are not optional. Tests that depend on fixtures are not
optional. There is no "future work" exit.
---
## 1. Test corpus
The schema authority for this version of the simulator is a single
production bundle from the `ds-inference` branch's N=3 vastai
investigation (see `examples/pipeline-parallel-inference/N3_DEPLOYMENT_REPORT.md`).
That bundle is the floor: every record kind present in it must be
emittable by the simulator; every field present in it must be
populated or explicitly `None` per OBSERVABILITY §6 rule 1; every
schema version in it must be readable by the post-processor against
which the simulator is checked.
### 1.1 Location
```
crates/simulation/tests/parity-bar/fixtures/
vastai-n3-1/ # the reference bundle (unpacked tar)
MANIFEST.json
orchestrator/{boot,snapshots/,events/,finalize}.json
stage-0/{...}
stage-1/{...}
stage-2/{...}
collector.log
PROVENANCE.md # how this bundle was produced; do not touch
```
### 1.2 Corpus scope
This is the *only* prod-origin corpus the simulator is checked
against in v1. Schema-floor coverage (§6) is bounded by what this
bundle exposes; the simulator is not obligated to emit record kinds
that do not appear here, and is *forbidden* from emitting record
kinds that do not appear in either this bundle or OBSERVABILITY §3.
When additional prod bundles are checked in, the corresponding
sections of §6 expand. The expansion is a separate, human-gated
commit (§12.1).
### 1.3 Bundle modifications
The fixture bundle is immutable. The agent does not touch any file
under `tests/parity-bar/fixtures/`. Repairs to the corpus (e.g., a
field renamed in the schema) happen in a separate commit that also
updates the lock hash in §12.1.
---
## 2. Determinism oracle
Discharges SPEC §2.1, §2.4, §2.5.
### 2.1 Two-run byte equality
```
crates/simulation/tests/parity-bar/t_determinism.rs::two_run_byte_equality
```
Two invocations of the engine with the same `(topology spec, seed)`
produce two bundles whose every regular file has the same SHA-256.
Includes NDJSON byte order, tar header timestamps if present, and
the contents of `sim/` (§6.4 in SPEC).
Binary: zero file-level hash differences. Failure must name every
diverging path.
### 2.2 Two-process concurrent equality
```
crates/simulation/tests/parity-bar/t_determinism.rs::two_process_equality
```
The same `(spec, seed)` run in two separate OS processes concurrently
(spawned by the test harness, `std::process::Command` or equivalent)
produces byte-identical bundles. Catches non-determinism from
process-global state: allocator addresses, vtable layout under
ASLR, environment variable order, thread-local randomness.
Binary: zero file-level hash differences. The two processes must be
the same binary invoked twice; not two compilations.
### 2.3 Debug/release equality
```
crates/simulation/tests/parity-bar/t_determinism.rs::debug_release_equality
```
The same `(spec, seed)` run with `cargo run --release` and
`cargo run` (debug) produces byte-identical bundles. If this fails,
the cause is almost always floating-point ordering; the engine
must not use floating-point arithmetic on any code path whose
output reaches the recording.
Binary: zero file-level hash differences across profiles.
### 2.4 No wall-clock taint
```
crates/simulation/tests/parity-bar/t_determinism.rs::no_wall_clock_taint
```
Set `FAKETIME` (or equivalent libfaketime injection) to advance the
host clock by 1 hour. Run the engine. The bundle must be identical
to a run without the injection. Proves no kernel-time leak past the
facade (SPEC §2.4, `SystemTime`/`Instant` row).
Binary: zero file-level hash differences with vs. without injection.
### 2.5 Divergence detection
```
crates/simulation/tests/parity-bar/t_determinism.rs::divergence_detected
```
Run with `(spec, seed)`, store the bundle hash chain. Modify a
single byte in the engine's RNG output (via a `#[cfg(test)]`
poisoned-RNG facade switch — the *only* such test-only switch
permitted, and it must live in the sim facade crate). The divergence
detector (SPEC §2.5) must produce a structured `Error` event naming
the first divergent record and exit non-zero.
Binary: divergent run exits non-zero; clean run exits zero.
---
## 3. Replay isomorphism
Discharges SPEC §7.
### 3.1 Self-replay
```
crates/simulation/tests/parity-bar/t_replay.rs::self_replay_identical
```
Run the engine to produce bundle B₁. Pass B₁ to the engine in
replay mode (`--replay B₁/`). Produce B₂. B₂ must be byte-identical
to B₁.
This is the load-bearing test of recording self-sufficiency: any
piece of state needed to drive a replay must be in the recording.
If B₂ differs from B₁, the recording is missing information SPEC
§7.1 demands it carry.
Binary: zero file-level hash differences.
### 3.2 Mutation fidelity
```
crates/simulation/tests/parity-bar/t_replay.rs::mutations_preserved
```
A topology spec declaring `partition(at_ms=5000)`, `heal(at_ms=8000)`,
`restart(node=stage-1, at_ms=10000)` is run. The resulting bundle
must contain `Custom` mutation events for each, with virtual-time
`wall_ms` exactly matching the spec, in spec order, on the
recording streams SPEC §4.1 and §5.3 designate.
Binary: exact count, exact ordering, exact `wall_ms` per mutation.
### 3.3 Recording self-sufficiency under prod shape
```
crates/simulation/tests/parity-bar/t_replay.rs::replay_from_prod_shape_only
```
Run the engine to produce bundle B. Delete the `sim/` subtree from
B (SPEC §6.4 — the sim-only fields). Pass the stripped bundle to
replay. The replay must complete; the resulting bundle's non-`sim/`
files must be byte-identical to B's non-`sim/` files.
This proves replay does not depend on sim-only data leaking into
the prod-shape part of the recording. A prod bundle, which never
has `sim/`, must replay just as well.
Binary: zero file-level hash differences on non-`sim/` files;
replay exits zero.
### 3.4 Prod-bundle replay (corpus-anchored)
```
crates/simulation/tests/parity-bar/t_replay.rs::vastai_n3_replays
```
Pass `tests/parity-bar/fixtures/vastai-n3-1/` to the engine in
replay mode. The engine must:
- Reconstruct topology and peer set from the bundle's boot blocks
(SPEC §7.1).
- Reconstruct mutation schedule from `Custom` events in the bundle.
- Complete the replay without panicking.
- Emit a bundle in the same layout (§6.4, §7.1).
This test does *not* require the emitted bundle to be byte-identical
to the input bundle. It is sufficient that the replay completes and
produces a structurally valid output. Statistical comparison
between input and output is a calibration concern (SPEC §9) and is
out of scope for this spec (§15).
Binary: replay exits zero; output bundle passes §7 checks.
---
## 4. Facade integrity
Discharges SPEC §3, §2.4.
### 4.1 Banned-API lint
```
crates/lint-deterministic/ # new crate
crates/lint-deterministic/banned.toml # the closed list
crates/lint-deterministic/tests/banned_apis_rejected.rs
```
A custom `cargo xtask lint-deterministic` step runs in CI before any
test. It rejects the workspace build if any module outside the
sim-facade implementation references the banned-API set. The set
is closed and version-locked in `banned.toml`. The current set:
```
std::time::SystemTime → Facade::clock().now()
std::time::Instant → Facade::clock().now()
std::thread::sleep → Facade::clock().sleep_until(...)
std::thread::spawn → Facade::spawn(...)
tokio::spawn → Facade::spawn(...)
tokio::time::sleep → Facade::clock().sleep_until(...)
tokio::time::Instant → Facade::clock().now()
std::collections::HashMap → indexmap::IndexMap or BTreeMap
std::collections::HashSet → indexmap::IndexSet or BTreeSet
rand::thread_rng → Facade::rng(stream_label)
getrandom::getrandom → Facade::rng(stream_label)
std::env::var → Facade::env(name)
std::env::vars → Facade::env_iter()
std::fs → Facade::fs() (sandboxed per node)
std::net → Facade::udp() / Facade::tcp()
std::process::Command → Facade::process(...) (errors in sim)
```
Allowlist (paths exempt from the lint):
```
crates/simulation/src/facade/sim/ # the sim facade itself
crates/runtime-facade/src/prod/ # the prod facade itself
crates/lint-deterministic/ # the lint itself
```
Binary: zero violations across the workspace.
### 4.2 Feature exclusivity
```
crates/simulation/tests/parity-bar/t_facade.rs::feature_exclusivity
```
A test in the workspace `xtask` confirms that `cargo build` with
features `facade-prod facade-sim` fails (compile error from a
`compile_error!` macro in the facade crate). The same test confirms
that `cargo build` with neither feature also fails.
Binary: both invocations exit non-zero with the expected error string.
### 4.3 No `cfg(sim)` in peer code
```
crates/lint-deterministic/tests/no_cfg_sim_in_peer.rs
```
```
grep -r 'cfg(\s*sim\s*)' \
crates/distribution \
crates/swactor \
crates/node \
crates/process \
examples/
# must return zero matches
```
Peer code never branches on whether it is in the sim. The facade
is the only swap point (SPEC §3.4). Code that compiles
conditionally on sim-vs-prod inside a peer crate is a violation.
Binary: zero matches.
### 4.4 Facade trait surface is closed
```
crates/runtime-facade/tests/surface_locked.rs
```
The trait family exposed by `runtime-facade` is fingerprinted (each
trait's method signatures hashed in declaration order, hashes
concatenated). The fingerprint is stored in
`crates/runtime-facade/surface.lock`. Changes to the trait surface
require updating this lock in a separate commit gated by §12.1.
Binary: live fingerprint equals locked fingerprint.
---
## 5. Same-binary invariant
Discharges SPEC §3.4, §4.6, §5.1.
### 5.1 Shared load-bearing dependencies
```
crates/simulation/tests/parity-bar/t_same_binary.rs::shared_load_bearing
```
`cargo metadata` for the prod binary and the sim driver must list
the following crates, at *identical resolved versions*, in both
dependency graphs:
```
iroh
iroh-relay
quinn
quinn-proto
swactor # the SWIM implementation
distribution::diagnostics
postcard # codec
```
The set is closed in this spec. Adding to it requires a §12.1
commit.
Binary: exact set equality at exact version equality on both sides.
### 5.2 No sim-only forks of transport code
```
crates/simulation/tests/parity-bar/t_same_binary.rs::no_transport_forks
```
The sim has zero source files matching any of:
- `**/sim_swim/**`
- `**/sim_iroh/**`
- `**/swim_sim.rs`
- `**/iroh_sim.rs`
- `**/quinn_sim.rs`
The transports under sim are the production transports linked
against the sim facade (SPEC §4.6). Replacement implementations
defeat the purpose of the sim.
Binary: zero matches.
### 5.3 Symbol overlap floor
```
crates/simulation/tests/parity-bar/t_same_binary.rs::symbol_overlap
```
`nm` (or `cargo bloat`) on the prod binary and the sim driver
produces two symbol sets. Their intersection must contain every
public symbol from the §5.1 crates. The check is exact: any
listed crate's public surface missing from either binary fails.
Binary: full inclusion in both directions.
---
## 6. Schema floor coverage
Discharges OBSERVABILITY §3, NORTH_STAR §"Parity bar". Scope bounded
by the v1 corpus (§1).
### 6.1 Corpus record-kind census
```
crates/simulation/tests/parity-bar/t_schema_coverage.rs::corpus_kinds
```
The reference bundle (§1.1) is parsed at test start. The set of
record kinds present is computed and stored as the required set.
A reference sim scenario (`tests/parity-bar/fixtures/scenarios/reference.toml`)
is run. Every record kind in the corpus set must be present in the
sim output.
This is the *floor*: the sim must emit at least what prod emits.
The agent does not need to invent record kinds the corpus does not
exhibit.
Binary: corpus kinds ⊆ sim-output kinds.
### 6.2 No phantom records
```
crates/simulation/tests/parity-bar/t_schema_coverage.rs::no_phantom_records
```
The sim must not emit record kinds that are not in either:
- The reference bundle (§1.1), or
- The schemas enumerated in OBSERVABILITY §3 / §4.
This prevents the sim drifting toward sim-specific schemas that
the prod side cannot consume. Sim-only fields (SPEC §6.5) live
under `sim/` and are excluded from this check.
Binary: sim-output kinds ⊆ (corpus kinds ∪ OBSERVABILITY-documented kinds).
### 6.3 Field presence audit
```
crates/simulation/tests/parity-bar/t_schema_coverage.rs::no_silent_none
```
For every record emitted by the reference scenario, every field
documented in OBSERVABILITY §3 must be either:
- Populated (non-null), or
- Explicitly `None` with at least one `Error` event somewhere in
the bundle whose `component` matches the introspector responsible
and whose `message` notes the gap.
A silent `None` (no accompanying `Error`) is a failure (OBSERVABILITY
§6 rule 1).
Binary: zero silent-None fields.
### 6.4 Variant exhaustiveness
```
crates/simulation/tests/parity-bar/t_schema_coverage.rs::all_event_variants_fire
```
The reference scenario (which is a multi-mode scenario combining
boot, steady-state, partition, heal, restart, and shutdown) must
fire every `Event` variant the corpus exhibits. Variants present
in OBSERVABILITY §3.3 but absent from the corpus are *not* required
in v1.
Binary: corpus variants ⊆ sim-emitted variants.
### 6.5 Reference scenario contents
```
crates/simulation/tests/parity-bar/fixtures/scenarios/reference.toml
```
A 60-second scenario carrying:
- 3 sim-native peers in a chain (mirrors the N=3 corpus layout)
- 1 orchestrator
- 1 collector
- Boot, steady-state SWIM ticks for 20s
- Partition at t=20s isolating stage-2
- Heal at t=35s
- Restart of stage-1 at t=45s
- Clean shutdown at t=60s
This scenario is the floor on which §6.1–§6.4 run. It is the only
scenario whose contents §6 binds to. New scenarios can be added in
`tests/exploratory/`; they do not extend the §6 contract.
---
## 7. Schema round-trip
Discharges SPEC §6, OBSERVABILITY §3.
### 7.1 Prod parser eats sim bundle
```
crates/simulation/tests/parity-bar/t_round_trip.rs::prod_parser_reads_sim_bundle
```
The reference-scenario sim bundle is passed to the production
post-processor (`crates/distribution/src/diagnostics/postproc`). The
post-processor must return `Ok(_)` and its `warnings` array must
have zero `unknown_field` and zero `unknown_record_kind` entries.
Binary: post-processor exit zero, zero unknown-* warnings.
### 7.2 Sim parser eats prod bundle
```
crates/simulation/tests/parity-bar/t_round_trip.rs::sim_replay_parser_reads_prod_bundle
```
The corpus bundle (§1.1) is passed to the sim's replay loader.
The loader must return `Ok(_)` and emit zero `unknown_field` /
`unknown_record_kind` warnings. The sim parser and the prod parser
are the *same code* (per SPEC §8.1) — this test exists to confirm
that fact, not to test a separate codepath.
Binary: replay-loader exit zero, zero unknown-* warnings.
### 7.3 Bundle layout exact
```
crates/simulation/tests/parity-bar/t_round_trip.rs::bundle_layout_matches_spec
```
`tar tf` of the reference sim bundle is compared against the
expected file list:
```
{run_id}/
MANIFEST.json
orchestrator/boot.json
orchestrator/snapshots/*.json
orchestrator/events/*.json
orchestrator/finalize.json
stage-N/boot.json # for each peer
stage-N/snapshots/*.json
stage-N/events/*.json
stage-N/finalize.json
sim/spec.toml
sim/seed
sim/links_applied.json
sim/mutations.log
sim/wire/*.ndjson # if wire capture is enabled
```
Files that are not listed above must not be present. Files that are
listed but conditional (e.g., `wire/`) are present iff the run
config enabled them.
Note: the prod `collector.log` (SPEC §6.4) is not yet a binding
artifact in v1 — the collector binary itself lands with the
calibration loop (§15). The file's fixed bundle-root path conflicts
with §9.1's substring rename map (the engine writing a fixed
filename in both base and renamed runs forces a key mismatch under
`apply_path_rename`). v2 reintroduces the requirement once the
collector binary's diagnostics output names itself from the
collector host's spec name.
Binary: exact match modulo conditional sections.
### 7.4 Schema version pin
```
crates/simulation/tests/parity-bar/t_round_trip.rs::schema_version_pinned
```
Every record envelope's `schema_version` field equals the version
exported as `distribution::diagnostics::SCHEMA_VERSION`. The
constant is single-source.
Binary: zero records with a divergent `schema_version`.
---
## 8. Causality and time invariants
Discharges SPEC §2.2, §2.3, OBSERVABILITY §3.3.
### 8.1 `monotonic_seq` strictly increasing
```
crates/simulation/tests/parity-bar/t_causality.rs::monotonic_seq_strictly_increasing
```
For each `(node_id, boot_sequence)` in the reference bundle, the
sequence of `monotonic_seq` values across all records (events and
snapshots) is strictly increasing and contiguous (no gaps that
would imply lost records).
Binary: zero violations.
### 8.2 `wall_ms` non-decreasing modulo declared jumps
```
crates/simulation/tests/parity-bar/t_causality.rs::wall_ms_non_decreasing
```
For each node, the sequence of `wall_ms` values is non-decreasing,
except across instants marked by a `Custom { kind: "clock_jump" }`
event (which the sim emits when a topology-declared clock jump
fires per SPEC §4.7). Across each clock_jump, the delta in
`wall_ms` is allowed; before and after, monotonicity is required.
Binary: zero unexplained non-monotonic transitions.
### 8.3 Send precedes receive
```
crates/simulation/tests/parity-bar/t_causality.rs::send_precedes_receive
```
For every `MessageReceived { peer = P, kind = K, ... }` on node A,
there exists a `MessageSent { peer = A, kind = K, ... }` on node P
whose `wall_ms` is at least `link(P→A).one_way_delay_ms − link.jitter_ms`
earlier (in virtual time). The match is by `(peer, kind, size,
trace_id?)` — once causal trace IDs land (OBSERVABILITY §4.2),
the match becomes exact.
Binary: every received-message record has a matching causal sender.
### 8.4 `snapshot_id` uniqueness
```
crates/simulation/tests/parity-bar/t_causality.rs::snapshot_id_unique
```
Across the entire bundle, no two snapshots share a `snapshot_id`.
Binary: set size equals list length.
### 8.5 Tiebreaker order
```
crates/simulation/tests/parity-bar/t_causality.rs::executor_tiebreaker_deterministic
```
Construct a scenario in which three events fire at the same
virtual tick on the same node. The executor's order of resolution
must follow `(node_id, fiber_id, event_seq)` per SPEC §2.3. The
test runs the scenario twice and asserts the per-tick resolution
order is identical.
Binary: identical order across two runs.
---
## 9. Equivariance
Discharges SPEC §4.1.
### 9.1 Node-id rename invariance
```
crates/simulation/tests/parity-bar/t_equivariance.rs::rename_invariance
```
Spec₁ defines nodes `[alpha, beta, gamma]` with a given topology
graph. Spec₂ is identical but renames `[alpha→x, beta→y, gamma→z]`,
applying the rename consistently to every peer-set reference.
Both run with the same seed.
The bundles must be byte-identical after applying the rename map
to the output (file paths under `{node_id}/` get renamed, and
every occurrence of the old `node_id` strings in record contents
gets substituted).
Binary: zero diffs after rename-map application.
### 9.2 Spec ordering invariance
```
crates/simulation/tests/parity-bar/t_equivariance.rs::spec_ordering_invariance
```
Spec₁ lists nodes in `[alpha, beta, gamma]` order; spec₂ lists the
same nodes in `[gamma, alpha, beta]` order; same seed. The
canonical ordering (sorted by `node_id`) used by the engine means
the bundles must be byte-identical.
Binary: zero diffs.
### 9.3 Same-tick mutation reordering
```
crates/simulation/tests/parity-bar/t_equivariance.rs::same_tick_mutation_reorder
```
Two mutations scheduled at the same virtual tick (e.g.,
`partition(at_ms=5000, edges=[a-b])` and `partition(at_ms=5000, edges=[b-c])`)
are specified in two different orders across two spec files; same
seed. The bundles must be byte-identical.
Binary: zero diffs.
---
## 10. Adversarial sim-detector
Discharges NORTH_STAR §"Parity bar" ("must have no means, statistical
or otherwise, of detecting that it is in a sim").
### 10.1 Detector binary
```
crates/sim-detector/ # new crate
crates/sim-detector/src/lib.rs # enumerated detection techniques
crates/sim-detector/src/main.rs # peer binary
```
The detector is a peer binary linked through the runtime facade.
It compiles in both `facade-prod` and `facade-sim` configurations.
Its job is to report, for each technique, one of:
```
enum Verdict {
Indistinguishable,
DetectedSim(&'static str),
DetectedProd(&'static str),
}
```
A passing sim run is one where every technique returns
`Indistinguishable` or `DetectedProd`. A passing prod run is one
where every technique returns `Indistinguishable` or `DetectedSim`.
The detector exists to be wrong about *which* environment it is in
— it must never correctly identify the sim.
### 10.2 Closed enumerated technique set
The v1 set (closed; extensions require §12.1):
| ID | Technique |
|-----|------------------------------------------------------------|
| D01 | `SystemTime::now()` drift across `sleep(1s)` |
| D02 | `Instant::now()` vs facade clock alignment |
| D03 | `HashMap` iteration order across two identical inserts |
| D04 | `Box<dyn Trait>` vtable address stability across runs |
| D05 | `getrandom` entropy distribution test (chi-squared shape) |
| D06 | Spawn ordering on 1000 tasks in a tight loop |
| D07 | UDP loopback timing vs declared link delay |
| D08 | DNS resolution latency vs declared resolver latency |
| D09 | Hostname / process-pid uniqueness across two peers |
| D10 | Allocator address-range stability across runs |
| D11 | TCP loopback handshake count (sim TCP per SPEC §4.5) |
| D12 | `Thread::id()` stability across two facade `spawn`s |
The detector binary, run inside the reference scenario as an
additional peer, must produce verdicts that satisfy the §10.1 rule.
```
crates/simulation/tests/parity-bar/t_detector.rs::sim_indistinguishable
crates/simulation/tests/parity-bar/t_detector.rs::prod_baseline
```
Binary: zero `DetectedSim` verdicts in sim runs; zero `DetectedProd`
verdicts in prod runs. `prod_baseline` is a sanity check that runs
the detector against the prod facade and confirms it does *not*
falsely report sim there.
### 10.3 Technique provenance
Every entry in §10.2 is documented in
`crates/sim-detector/src/techniques.md` with:
- The exact mechanism (which API, what value range constitutes detection).
- The reason it would betray a naive sim.
- The facade obligation that prevents it.
The agent must update this file when adding a technique (and must
not add techniques without a §12.1 commit).
---
## 11. Lifecycle observability
Discharges SPEC §5.3.
### 11.1 Boot-record presence
```
crates/simulation/tests/parity-bar/t_lifecycle.rs::start_emits_boot
```
Every `start_at_ms` in the spec produces exactly one `boot.json`
under that node's directory, with `boot_sequence = 0`.
Binary: one boot record per start declaration; `boot_sequence` correct.
### 11.2 Restart sequence integrity
```
crates/simulation/tests/parity-bar/t_lifecycle.rs::restart_increments_boot_sequence
```
For a node with `start_at_ms=0, restart_at_ms=[10000, 20000]`, the
bundle contains:
- `boot.json` with `boot_sequence=0` and a `finalize.json` ending the first epoch.
- A second epoch with `boot.json` `boot_sequence=1` and a `finalize.json`.
- A third epoch with `boot.json` `boot_sequence=2` (still running at end-of-run
so finalize may be absent if the run ends mid-epoch — see §11.5).
Binary: exact count, exact `boot_sequence` values, exact order.
### 11.3 Crash distinguishability
```
crates/simulation/tests/parity-bar/t_lifecycle.rs::crash_omits_finalize
```
A node with `crash_at_ms=5000` and no further restart produces a
`boot.json` and *no* `finalize.json`. The engine emits a sim-only
`Custom { kind: "crash" }` record on the recording so the
post-processor can distinguish crash from incomplete capture.
Binary: missing `finalize.json` exactly when crashed; `Custom`
record present.
### 11.4 Clean shutdown
```
crates/simulation/tests/parity-bar/t_lifecycle.rs::stop_emits_clean_finalize
```
A node with `stop_at_ms=5000` produces a `finalize.json` whose
`shutdown_reason = "clean"` (or whatever the prod schema names
clean shutdown).
Binary: `finalize.json` present with correct reason.
### 11.5 Mid-run finalization
```
crates/simulation/tests/parity-bar/t_lifecycle.rs::end_of_run_finalizes_all_living
```
When the engine reaches its terminal virtual time with nodes still
running, each living node receives a synthetic clean shutdown and a
`finalize.json` is written. The synthetic shutdown is marked
(`shutdown_reason = "end_of_run"`).
Binary: every node has either an organic finalize, a crash mark, or
an `end_of_run` finalize. No node ends a run with neither.
---
## 12. Test infrastructure integrity
Discharges this document itself.
### 12.1 Parity directory hash lock
```
scripts/check-parity-lock.sh
tests/parity-bar/.locked-hashes
```
CI's first step computes the recursive SHA-256 of every file under
`crates/simulation/tests/parity-bar/` (excluding `.locked-hashes`
itself) and compares against the value committed in `.locked-hashes`.
Mismatch fails the build with an instruction to run
`scripts/update-parity-lock.sh` in a *separate* commit titled
`parity-bar: update lock`.
This prevents the agent from quietly weakening tests. The same
guard applies to this file: `crates/simulation/TESTING_SPEC.md` is
inside the lock.
Binary: computed hash equals committed hash.
### 12.2 No probabilistic primitives in parity tests
```
crates/lint-deterministic/tests/no_probabilistic_primitives.rs
```
Within `crates/simulation/tests/parity-bar/`, the following are
banned (compile-error if present):
```
proptest::*
quickcheck::*
rand::random
rand::thread_rng
fn fuzz_*
```
Parity-bar tests use seeded, committed inputs only. Probabilistic
testing belongs in `crates/simulation/tests/exploratory/`, which is
not part of this spec's binary checks.
Binary: zero matches.
### 12.3 No conditional skips in parity tests
```
crates/lint-deterministic/tests/no_skips_in_parity.rs
```
Within `tests/parity-bar/`, the following are banned:
```
#[ignore]
#[cfg(not(...))] # any cfg gate
if std::env::var(...).is_ok() { return; } # env-driven skip
#[cfg_attr(..., ignore)]
```
Binary: zero matches.
### 12.4 No `unwrap()` masking in parity tests
```
crates/lint-deterministic/tests/parity_tests_use_explicit_assert.rs
```
Within `tests/parity-bar/`, `.unwrap()` on `Result` returned by
engine APIs must be `.expect("<reason>")` with a reason string. This
is a readability lint, not a correctness one, but parity-test
failures must be debuggable from CI output alone — the agent will
not be there to add prints when something fails.
Binary: zero `Result::unwrap()` calls without messages.
### 12.5 Reference scenario locked
The reference scenario (§6.5) and the fixture bundle (§1.1) are
inside the parity directory and therefore are §12.1-protected.
---
## 13. Definition of done
The simulator is considered built when:
- The full check list in §2–§12 runs in CI via `cargo xtask parity-bar`.
- Every check is green.
- The `parity-bar` job is required on the `ds-inference` branch
before merge.
- `tests/parity-bar/PROVENANCE.md` documents the corpus and its
source run.
- `crates/sim-detector/src/techniques.md` documents each detection
technique in §10.2.
- `crates/lint-deterministic/banned.toml` enumerates §4.1's set.
- The reference scenario (§6.5) runs end-to-end and produces a
bundle passing §6, §7, §8, §11.
- The corpus replay (§3.4) succeeds.
The agent does *not* mark the simulator built when:
- Any check is `#[ignore]`d, gated, or partially-skipped.
- Any check has been replaced with a weaker version.
- Any check passes only on debug or only on release.
- Any check passes only with a specific `RUSTFLAGS` setting.
- Any check requires manual setup not encoded in `cargo xtask`.
Calibration (SPEC §9) is explicitly out of scope for v1's done
criterion. The deterministic substrate this spec defines is what
makes calibration meaningful later.
---
## 14. Failure protocol
When a check defined in this spec fails, the agent's response is
exactly one of:
1. **Fix the simulator** to satisfy the check. This is the default.
2. **Fix the check** by raising a defect in this document, *if and
only if* the check is wrong in a way that an independent reader
can verify against NORTH_STAR / SPEC / OBSERVABILITY. The fix is
a separate commit titled `TESTING_SPEC: correct §N.M`, including
a justification quoting the upstream doc.
The agent's response is *never*:
- `#[ignore]` the test.
- Replace the test with a weaker one.
- Add a `// TODO: re-enable when …` marker.
- Move the test to `exploratory/`.
- Edit `.locked-hashes` directly.
- Loosen a banned-API list.
- Skip the test on a specific platform or feature.
- Mark the check "blocked on external work" and proceed.
If the agent finds itself unable to fix the simulator and unable to
write a justified spec correction, the work stops. The artifact
checked in is the partial work plus a `BLOCKED.md` in the simulation
crate root naming the check, the symptom, and the obstacle. This is
the only acceptable exit short of green.
---
## 15. Out of scope (v1)
The following are *not* obligations under this spec. The simulator
is not required to satisfy any check below for v1 to be considered
built. Each will become a v2 obligation as the corpus and the
calibration loop mature.
- **Statistical / distributional parity.** KS tests, mean / p99
parity, tail-shape parity. These are calibration (SPEC §9), gated
by the noise-floor estimation work that needs a multi-bundle
corpus.
- **Multi-bundle corpus.** v1's schema floor (§6) is bounded by the
single vastai-n3 bundle. Adding bundles widens the floor.
- **Wire-level packet trace** (OBSERVABILITY §4.1). Optional in v1;
the sim implements §4.1 only if a check in §6 binds to it (it does
not, in v1).
- **Causal trace IDs** (OBSERVABILITY §4.2). Future surface. §8.3's
send-precedes-receive check uses heuristic matching; once trace
IDs land, the check becomes exact and §8 expands.
- **Opaque-binary hosts** (SPEC §5.2). v1 ships with sim-native
peers only. The opaque-binary escape hatch is a v2 obligation.
- **Cross-architecture determinism.** v1 commits to determinism on
one architecture (linux-x86_64); same-architecture / different-machine
determinism is the §2 obligation. Cross-arch is a v2 obligation
if and when a target requires it.
- **Statistical detector techniques.** §10.2's set is structural /
behavioral. Statistical detectors (e.g., "the variance of inter-message
intervals matches a sim-implausible distribution") are a v2
expansion.
Anything listed above that the agent finds an unforced opportunity
to satisfy is welcome to satisfy. None is required.
---
## 16. Glossary
Specific to this spec; for engine / sim terms see SPEC §10.
- **Binary check.** A test whose outcome is pass or fail with no
intermediate states, thresholds, or knobs. The unit of contract in
this document.
- **Closed list.** A set defined explicitly in this document, locked
by §12.1, modifiable only by a separate commit that itself updates
the lock. New items can be added; existing items cannot be removed
or weakened.
- **Corpus.** The set of prod-origin bundles the simulator is checked
against. v1 corpus is §1.1.
- **Detector.** A peer binary (§10.1) attempting to identify whether
it runs in sim or prod. Used to enforce NORTH_STAR's
"indistinguishable" requirement as an executable test.
- **Facade.** Per SPEC §3 / §10. Banned-API allowlist scopes
reference this term.
- **Lock.** The SHA-256 hash committed in
`tests/parity-bar/.locked-hashes` covering every file in the
parity-bar directory, this document included.
- **Parity directory.** `crates/simulation/tests/parity-bar/`. The
only directory whose contents are bound by this spec.
- **Reference scenario.** §6.5. The single scenario whose execution
is observed by the schema-floor (§6), schema-round-trip (§7),
causality (§8), and lifecycle (§11) checks.