Determinism fixtures
Reproducibility is the foundation every other part of the testing stack builds on. If two runs of the same test produce different seeds, different timestamps, or different run IDs, then no frame-checksum comparison, no shadow-run, and no soak test can be trusted.
ftui-harness centralises determinism in a single type,
DeterminismFixture, driven by three
environment variables.
Source: crates/ftui-harness/src/determinism.rs.
The three variables
| Variable | Type | Default | Effect |
|---|---|---|---|
E2E_DETERMINISTIC | bool-ish (1, true, yes) | unset = off | Turns on deterministic mode. Swaps wall-clock timestamps for T{n:06} counters and swaps PID-based run IDs for {prefix}_seed{seed}. |
E2E_SEED | u64 | fixture default (e.g. 0) | Seeds both RNGs and the deterministic run_id. Tests that thread a seed through their sub-systems should read this. |
E2E_TIME_STEP_MS | u64 | 16 | Cadence of LabSession::now_ms() advances. Also drives deterministic tick spacing. |
All three variables are off by default in cargo test. They are
switched on automatically by the scripts/*.sh entry points that
need them (see E2E scripts).
When to use each
E2E_DETERMINISTIC
Turn on determinism whenever the output is going to be diffed or stored as evidence. That means:
- Shadow-run tests (
ShadowRun::compare) - E2E scripts that emit JSONL for evidence sink inspection
- Determinism-soak runs (determinism soak)
- CI gate runs (
scripts/e2e_test.shsets it; see E2E playbook )
E2E_DETERMINISTIC=1 cargo test -p ftui-harness shadow_ratatui_e2eWithout this flag, run_id contains the process PID and the wall-clock
seconds — both of which change every run.
The DeterminismFixture type
pub struct DeterminismFixture {
seed: u64,
deterministic: bool,
time_step_ms: u64,
run_id: String,
ts_counter: AtomicU64,
ms_counter: AtomicU64,
start: Instant,
}Constructed with:
let fx = DeterminismFixture::new("doctor_happy", /* default_seed */ 0);new reads E2E_DETERMINISTIC, E2E_SEED, and E2E_TIME_STEP_MS
from the environment once and caches them. Subsequent reads on the
same fixture are consistent.
Methods
| Method | Purpose |
|---|---|
seed() | The effective seed (env override or default_seed). |
deterministic() | true when E2E_DETERMINISTIC is set. |
run_id() | Stable run identifier — {prefix}_seed{seed} in deterministic mode, otherwise {prefix}_{pid}_{unix_secs}. |
timestamp() | T{n:06} counter in deterministic mode, wall-clock seconds otherwise. |
now_ms() | Monotonic ms; advances by time_step_ms per call in deterministic mode. |
env_snapshot() | EnvSnapshot with TERM, COLORTERM, NO_COLOR, TMUX, ZELLIJ, seed, and deterministic flag. |
Scenario counter
Lab::run_scenario increments a global counter every time it runs a
scenario. Read it with:
use ftui_harness::determinism::lab_scenarios_run_total;
let before = lab_scenarios_run_total();
// ... run scenarios ...
let after = lab_scenarios_run_total();
assert!(after > before);LAB_RECORDINGS_TOTAL and LAB_REPLAYS_TOTAL play similar roles for
record/replay flows, exposed through lab_recordings_total() and
lab_replays_total().
What determinism does not cover
Terminal capability detection is environmental. TERM,
COLORTERM, NO_COLOR, TMUX, and ZELLIJ are read by
ftui-core’s capability detector. If you want a stable capability
profile, pin it with FTUI_TEST_PROFILE=modern (or similar) — see
snapshot tests.
External RNGs. rand::thread_rng(), std::time::SystemTime::now(),
uuid::Uuid::new_v4() — none of these consult the fixture. Route
anything nondeterministic through a seeded ChaCha8Rng or
LabSession::now_ms() before asserting on it.
Subprocess clocks. A child process started without env
propagation reads its own wall clock. The doctor_frankentui capture
layer threads these variables through explicitly; your harness must
do the same if it spawns children.