Skip to content

Stop exitTimelineMode's test leaking a load that gates the poll - #213

Merged
thalida merged 2 commits into
mainfrom
fix/issue-212-timeline-poll-flake
Sep 3, 2026
Merged

Stop exitTimelineMode's test leaking a load that gates the poll#213
thalida merged 2 commits into
mainfrom
fix/issue-212-timeline-poll-flake

Conversation

@thalida

@thalida thalida commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes #212

The failure

tests/hooks/useTimelineMode.test.ts > live poll suspends in Timeline mode > ... and resumes on exit fails intermittently on main:

AssertionError: expected "fetch" to be called at least once
  tests/hooks/useTimelineMode.test.ts:361:22

Twice in three weeks, same assertion both times: run 31991836275 (2026-08-17) and run 33795322008 (2026-09-03). The second landed on bb586d33, which touches no app/ or api/ file — the test and the code under test were byte-identical to what passed on that PR.

Cause

exitTimelineMode's suite fires a live-HEAD reload it never awaits:

exitTimelineMode();          // -> void loadSource(...), never awaited
await flush();
expect(StubEventSource.instances.length).toBeGreaterThan(0);   // stream opened, not finished

loadSource sets SCAN_PROGRESS before its first await (useManifestSource.ts:161) and clears it only in finally (line 214). The stub EventSource emits nothing unless a test calls emit(), so the load parks on the stream forever, the finally never runs, and SCAN_PROGRESS stays set for the rest of the file.

Measured, not inferred — a probe after that suite:

[probe] immediately after exitTimelineMode: {"kind":"local","phase":null}
[probe] after flush:                        {"kind":"local","phase":null}
[probe] after 5 real ticks:                 {"kind":"local","phase":null}

tick() yields to a foreground load on exactly that signal (useManifestSource.ts:289):

if (inFlight) return;
if (TIMELINE_MODE.peek()) return;
if (SCAN_PROGRESS.peek() !== null) return;   // <- this one

So every later test that asserts the poll fires depends on having nulled SCAN_PROGRESS in its own beforeEach. Three describes in the file already do that defensively, which is the tell. vitest.config.js sets sequence: { shuffle: true } precisely to surface couplings like this — "a test that only passes after its neighbour ran says so."

Fix

Cancel the load in afterEach. streamManifest already honours the abort signal (api/manifest.ts:175) and loadSource reads it as a clean user cancel, so the finally runs and SCAN_PROGRESS clears. No production change: the leak is entirely in the test's lifecycle.

The added expect(SCAN_PROGRESS.value).toBeNull() is the guard, not decoration.

Verification

  • File: 17/17
  • Full suite: 191 files, 2153 tests, exit 0
  • prettier --check clean
  • The guard bites — drop the cancelLoad() and it goes red:
    AssertionError: expected { kind: 'local', …(2) } to be null
    Tests  1 failed | 16 passed (17)
    
    Restored: 17/17.

What this does not claim

I could not reproduce the CI failure locally: 60 shuffled runs of the file and two full-suite runs with coverage all stayed green. This removes a proven order-coupling that is the most likely cause, not a confirmed reproduction of that specific run. If it recurs, the next suspect is inFlight (the exclude-refresh effect at useManifestSource.ts:334 sets it and clears it only in a .finally()), since SCAN_PROGRESS is now accounted for.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ

thalida and others added 2 commits September 3, 2026 12:32
The suite calls exitTimelineMode(), which fires a live-HEAD reload it never
awaits. loadSource sets SCAN_PROGRESS before its first await and clears it only
in `finally`, and the stub EventSource never emits, so that load parks forever
and leaves SCAN_PROGRESS set for the rest of the file.

tick() yields to a foreground load on exactly that signal, so any later test
asserting the poll fires depends on having nulled SCAN_PROGRESS itself. The
config shuffles test order on purpose to surface couplings like this, and the
live-poll resume test has failed twice on main this way (runs 31991836275 and
33795322008).

Cancelling settles the load through the abort path streamManifest already
honours, so its `finally` runs. The added assertion is the guard: drop the
cancelLoad() and it fails with `expected { kind: 'local', … } to be null`.

Closes #212

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ
@thalida
thalida merged commit b148ced into main Sep 3, 2026
5 checks passed
@thalida
thalida deleted the fix/issue-212-timeline-poll-flake branch September 3, 2026 19:46
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.

Flaky: useTimelineMode live-poll resume fails when a leaked loadSource writes SCAN_PROGRESS

1 participant