● Open sourceDeterministic Simulation Testing

nondet.

Turn a flaky test into an integer

A test runner that owns every source of nondeterminism — the microtask queue, timers, Date.now(), sockets — so a whole run becomes a pure function of one 64-bit seed.

TypeScriptNode 22+Zero deps38 tests
Language TypeScriptTests 38License MIT
nondet — node --test
$ node --test
✓ 253 seeds passed
✗ seed 254 failed
computed 2 times, expected 1
↳ shrunk 41 decisions → 3
01 The problem

Your suite passes 500 times, then fails once in CI.

And you never reproduce it. The failure depends on which of two tasks happened to wake first, and that ordering is decided by a clock you do not control. So the test gets marked flaky, retried, and eventually deleted — along with the bug it was catching.

02 The hard part

Where the work actually was.

V8's microtask queue is already deterministic. Two runs feeding the same values into the same awaits resolve in the same order every time, so that is not where flakiness comes from. It comes from the edges — when a timer fires, when a socket completes, what the clock said. So nondet takes the edges and leaves V8 alone, which means no compiler plugin and no rewriting of your awaits.

1

Seeded scheduler

When several tasks are eligible at once, the seed decides who wakes first. That is the interleaving fuzzer.

2

Time teleports

The clock jumps to the earliest pending timer once nothing can run. An eight-hour retry backoff finishes in about three milliseconds.

3

Deadlock detection

Nothing eligible, no timers, tasks still alive — you get every task and what it is blocked on, instead of a hung CI job.

4

Choice-stream shrinking

Every decision draws from one integer stream, so generic passes reduce schedules, network faults and generated data at once.

5

Simulated network

Latency, drops, duplicates and partitions, all seeded. A dropped packet is as reproducible as an off-by-one.

6

Patched globals

setTimeout, Date.now and Math.random are virtualised, so third-party code runs on virtual time unchanged.

03 Measured

What it actually does.

From examples/race-condition.ts, which CI runs on every push:

running 500 seeds ····· seed 253 failed passed 253 seeds before this one seed computed 2 times, expected 1 shrunk computed 6 times, expected 1 replaying seed 253 ten times ✓ 1 distinct outcome in 10 runs with single-flight de-duplication ✓ 2000 seeds, no failures
04 Engineering

How it's built.

LanguageTypeScript, no build step
PRNGPCG32, exact 64-bit limb arithmetic
Schedulerdrain → seeded pick → teleport
ShrinkingHypothesis-style choice stream
Tests38, plus a determinism job in CI
Prior artFoundationDB, TigerBeetle, madsim
05 Honestly

What it doesn't do.

Worker threads and real I/O are out of scope — anything that leaves the process is nondeterminism it cannot own. And it does not prove correctness: it explores interleavings you would never hit by hand, but a seed space is not a proof. This is fuzzing with a very good reproduction story.

Want a closer look?

The README goes deeper, and the tests are the honest documentation. Happy to walk through the trade-offs.