| 1 | // Run: tsx src/__tests__/turn-timing-race.test.ts |
| 2 | |
| 3 | import { initialState, promptEventClock, reducer } from "../lib/useController"; |
| 4 | |
| 5 | let passed = 0; |
| 6 | let failed = 0; |
| 7 | |
| 8 | function eq(actual: unknown, expected: unknown, label: string) { |
| 9 | if (actual === expected) { |
| 10 | process.stdout.write(` PASS ${label}\n`); |
| 11 | passed += 1; |
| 12 | } else { |
| 13 | process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}\n`); |
| 14 | failed += 1; |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | process.stdout.write("turn timing snapshot ordering\n"); |
| 19 | |
| 20 | const originalNow = Date.now; |
| 21 | let now = 2_000; |
| 22 | Date.now = () => now; |
| 23 | try { |
| 24 | let state = reducer(initialState, { |
| 25 | type: "event", |
| 26 | e: { kind: "turn_started", submissionId: "turn-a", turnStartedAt: 2_000 }, |
| 27 | }); |
| 28 | const staleSnapshotAt = promptEventClock(); |
| 29 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "turn-a" } }); |
| 30 | |
| 31 | const staleAfterDone = reducer(state, { |
| 32 | type: "backend_status", |
| 33 | running: true, |
| 34 | cancellable: true, |
| 35 | turnStartedAt: 2_000, |
| 36 | snapshotAt: staleSnapshotAt, |
| 37 | }); |
| 38 | eq(staleAfterDone.running, false, "a stale running snapshot cannot resurrect a completed turn"); |
| 39 | |
| 40 | now = 4_000; |
| 41 | state = reducer(state, { |
| 42 | type: "event", |
| 43 | e: { kind: "turn_started", submissionId: "turn-b", turnStartedAt: 4_000 }, |
| 44 | }); |
| 45 | |
| 46 | const staleRunning = reducer(state, { |
| 47 | type: "backend_status", |
| 48 | running: true, |
| 49 | cancellable: true, |
| 50 | turnStartedAt: 2_000, |
| 51 | snapshotAt: staleSnapshotAt, |
| 52 | }); |
| 53 | eq(staleRunning.turnStartAt, 4_000, "a stale running snapshot cannot rewind a newer turn clock"); |
| 54 | |
| 55 | const unorderedRunning = reducer(state, { |
| 56 | type: "backend_status", |
| 57 | running: true, |
| 58 | cancellable: true, |
| 59 | turnStartedAt: 2_000, |
| 60 | }); |
| 61 | eq(unorderedRunning.turnStartAt, 4_000, "an unordered status keeps a surviving newer local clock"); |
| 62 | |
| 63 | const staleIdle = reducer(state, { |
| 64 | type: "backend_status", |
| 65 | running: false, |
| 66 | cancellable: false, |
| 67 | snapshotAt: staleSnapshotAt, |
| 68 | }); |
| 69 | eq(staleIdle.running, true, "a stale idle snapshot cannot stop a newer live turn"); |
| 70 | |
| 71 | now = 5_000; |
| 72 | const correctedFallback = reducer({ ...state, turnStartAt: 3_900 }, { |
| 73 | type: "backend_status", |
| 74 | running: true, |
| 75 | cancellable: true, |
| 76 | turnStartedAt: 4_000, |
| 77 | }); |
| 78 | eq(correctedFallback.turnStartAt, 4_000, "a newer backend start still corrects an earlier local fallback"); |
| 79 | } finally { |
| 80 | Date.now = originalNow; |
| 81 | } |
| 82 | |
| 83 | if (failed > 0) { |
| 84 | process.stderr.write(`\n${failed} check(s) failed\n`); |
| 85 | process.exitCode = 1; |
| 86 | } else { |
| 87 | process.stdout.write(`\n${passed} passed, ${failed} failed\n`); |
| 88 | } |
| 89 |