Skip to content

Fix chunkErrorRecovery.spec.ts's reload-guard race in CI - #397

Merged
WilfordGrimley merged 1 commit into
masterfrom
fix-chunk-error-recovery-spec-determinism
Jul 23, 2026
Merged

Fix chunkErrorRecovery.spec.ts's reload-guard race in CI#397
WilfordGrimley merged 1 commit into
masterfrom
fix-chunk-error-recovery-spec-determinism

Conversation

@WilfordGrimley

Copy link
Copy Markdown

Symptom

frontend/tests/chunkErrorRecovery.spec.ts's expect.poll(() => reloadRequests).toBe(1) failed twice consecutively on CI shard 1/4 of PR #395 (run 30039392833), including a clean re-run, while passing reliably locally. Received: 0 - no error thrown anywhere in the test, the reload request simply never arrived.

Root cause

useChunkErrorRecovery's guard (chunkErrorRecovery.ts's CHUNK_RELOAD_GUARD_KEY, a real 10s sessionStorage-backed "only one reload per window" debounce) was consumed by a real chunk hiccup before the test's own synthetic dispatch ever ran.

CI's playwright.config.ts webServer runs npm run dev (not the static export the site actually deploys), and Next's dev server compiles pages on demand. Pulling the CI trace apart (playwright-report artifact from run 30039392833) showed the navbar's own "Editor" nav link - visible even while already on /editor - getting prefetched by next/link's default viewport IntersectionObserver behaviour, triggering a second on-demand recompile of pages/editor.js mid-test (~800ms, network-adjacent in time to the test's own dispatch). A slower/colder CI runner is more likely to still be mid-churn from that when the test body reaches its own dispatch, and any real transient chunk error during that churn legitimately (and correctly, per the guard's own design) consumes the one-shot budget first.

This is a dev-server/test-harness artifact only - the deployed static export has zero on-demand compilation or HMR, so it structurally can't happen in production. The guard suppressing a second reload within its window is the product working exactly as designed; the test's implicit assumption that no real chunk error has fired by the time it dispatches its own synthetic one is what's invalid in CI's slower/cold-compile environment.

Why the assertions are not weakened

Both guarantees the suite exists to protect stay exactly as strict:

  • One-reload guarantee: tests 1/2 (window 'error' and unhandledrejection triggers) still assert reloadRequests becomes exactly 1, unchanged.
  • No-infinite-loop guarantee: test 4 (two dispatches in quick succession) still asserts the guard suppresses the second reload for real - it only clears the guard once, before the first dispatch, specifically so the second dispatch still exercises real guard-suppression behaviour, not a cleared one.

The fix (clearReloadGuard, calling page.evaluate(() => sessionStorage.removeItem(CHUNK_RELOAD_GUARD_KEY)) immediately before each test's own dispatch) only establishes a clean precondition - it doesn't touch or bypass the guard logic itself. CHUNK_RELOAD_GUARD_KEY is exported from chunkErrorRecovery.ts as a pure string constant (no behaviour change) so the test doesn't duplicate/hardcode the key name.

Verification

  • npx prettier@2.7.1 --check on all three changed files: pass
  • npx tsc --noEmit: pass
  • npx jest (full suite, 64 suites / 565 tests, includes chunkErrorRecovery.test.ts's existing pure-logic coverage, untouched): all pass
  • npx playwright test tests/chunkErrorRecovery.spec.ts --repeat-each=10 (warm dev server): 40/40 passed
  • rm -rf .next && CI=true npx playwright test tests/chunkErrorRecovery.spec.ts --repeat-each=5 (cold dev-server cache, closer to CI's actual conditions): 20/20 passed
  • rm -rf .next && npx playwright test (full suite, cold cache): 328/328 passed

Deferred: reproducing the exact CI shard-1/4 timing on this machine wasn't attempted (single-machine timing races like this are inherently hard to force deterministically) - confidence instead comes from the CI trace's own network/timeline evidence (see docs/troubleshooting.md's new entry) plus the fix's mechanism directly targeting the exact state (the sessionStorage guard key) that had to be responsible given the observed symptom (zero network requests, zero errors, correct call ordering per the trace).

Docs

Added a symptom-first entry to docs/troubleshooting.md (chunkErrorRecovery.spec.ts's reload-guard race) per repo convention, since diagnosis here took well over 15 minutes.

Not merging - per this session's operating rules, PRs are reviewed and merged by the owner.

@WilfordGrimley
WilfordGrimley merged commit c594ae3 into master Jul 23, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant