2.8 KiB
2.8 KiB
name, description
| name | description |
|---|---|
| fixture-delay-window-race-test | Pin a race/abort bug in a fixture-server test by delaying only the Nth request so the interruption lands inside the in-flight window; assert via a driver-installed unhandledRejection reporter. |
Reproducing a timing-window bug against a fixture server
Use when a defect only appears in a window — a promise created before it is awaited, a kill/abort landing mid-flight, a prefetch interrupted — and a fixture HTTP server answers instantly, so the window never opens. Earned in dankvoice issue #7 ([Unhandled Rejection] AbortError from /say stop during a pipelined synthesis).
Why naive attempts pass vacuously
- Uniform delay on the fixture route (
DV_LATENCY_MSfor every request) makes all requests resolve at the same time, so by the time the interruption fires nothing is in flight. The test goes green against the broken code — worse than no test. - Asserting on a log line count that also counts interruption noise:
dankvoice's driver printsPLAYS <n>as the number of player log lines, and the stub appendsKILLEDon SIGTERM — so a single playback with one interrupt reportsPLAYS 2. Count the payload lines instead (grep -c 'PLAYER .*\.wav$').
The shape that works
- Delay only requests after the first. The first response gets the pipeline into playback; the second stays in flight, which is the window:
const spoken = received.filter((c) => c.url?.endsWith("/audio/speech")).length; // already pushed const delay = spoken > 1 ? Number(process.env.DV_SYNTH_MS ?? 0) : 0; if (delay > 0) setTimeout(answer, delay); else answer(); - Make the interruption land inside it. Player holds long (
DV_HOLD=2), interruption at a fixed offset (DV_STOP_AT=400) with the delayed response far later (DV_SYNTH_MS=1200): margins wide enough that the abort is neither before the request nor after the response. - Turn unhandled rejections into observable output in the test driver instead of letting the runtime crash on them — that is also exactly what the real harness does to the user:
Then assert
process.on("unhandledRejection", (reason) => { console.log(`UNHANDLED ${reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason)}`); });[[ "$output" != *"UNHANDLED"* ]]plus the concrete behavior (segments actually played, player killed). - Leak checks measure a delta, not an absolute: count
dankvoice-*temp dirs before and after the run and compare; an absolute== 0fails on another test's leftovers.
Order
Write the scenario first and watch it fail on the rejection assertion specifically — if it fails on some other assertion (a count, a kill line), the window is wrong, not the bug. Then fix, then confirm red→green on that one assertion before running the full suite.