fix(js-test): harden timing-fragile metron detail-fetch tests with vi.waitFor (bookshelf-wro8e) #1337
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-wro8e"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Root cause
Three 'METRON LAZY DETAIL' tests in
metadata_fetch_controller_comic.test.jsused fixed microtask drains —for (var i = 0; i < 8/16; i++) { await Promise.resolve(); }— to wait for async DOM mutations. The_fetchProviderpath goes through a multi-level promise chain (POST → resp.json() → pollJob → poll() → fetch status → resp.json() → _handleProviderResponse → render candidates). Under a loaded CI runner this chain requires more microtask ticks than the fixed count provides, so the.candidate-cardelement or the detail-fetch call inmock.callsisn't visible at assertion time. The test then hangs near the 10 s hook timeout (~9878 ms observed) or fails outright, nondeterministically.Fix
Replace the fixed
await Promise.resolve()drains (lines 142-145 and 157-160 and 172-175) withvi.waitFor(), which polls until the condition holds and resolves as soon as it does — making the tests deterministic regardless of microtask scheduling pressure. Under a fast local runvi.waitForresolves in its first poll tick (sub-millisecond). Under a loaded runner it keeps polling until the DOM catches up, well within vitest's default 1000 mswaitFortimeout.Tests that already use
vi.useFakeTimers()for the save-nav-timer path are unchanged.Before / after timing (local, fast machine)
Test plan
npm test(all 137 test files, 4610 tests): all passnpm run coverage(100% branch/statement/function/line gate on controllers/**): gate passesCloses bead bookshelf-wro8e on merge.
Security review of PR #1337 —
fix(js-test): harden timing-fragile metron detail-fetch tests with vi.waitFor(bookshelf-wro8e)Scope: single test file (
static/js/test/metadata_fetch_controller_comic.test.js), no production code changed.Findings
Production code surface
Zero. The diff touches exactly one file:
static/js/test/metadata_fetch_controller_comic.test.js. No Go, no templates, no SQL, no middleware, no routes. No production-code impact.Assertion strength after conversion
The three converted tests each replace a fixed microtask-drain loop (
for i < N; await Promise.resolve()) withvi.waitFor. The security-relevant assertions — that acandidate-detailfetch is actually triggered, that it containsprovider=metron, and that it containsexternal_id=12345— are preserved identically after the await. Thevi.waitForwrapper introduces a timeout-driven retry loop; if the fetch is never triggered the poll throws and the test fails, which is strictly stronger than the old fixed drain that could silently time out.Notably,
vi.waitForhere pollsglobal.fetch.mock.calls(the mocked fetch call log, not a real network call) — there is no network egress from these tests at all.external_id handling assertion
The third test (
detail fetch URL includes the metron issue external_id) specifically guards thatexternal_id=12345(matching the fixtureMETRON_CANDIDATE.external_id) reaches the candidate-detail URL. That assertion is unchanged and remains a hardexpect(detailCall).toContain("external_id=12345").Fixtures for secrets / PII
All fixture values are synthetic test data:
external_id: 12345,external_id: 567890, cover URL pointing tostatic.metron.cloudwith a generic path. No API tokens, session tokens, real user data, or PII.Test hygiene / package declaration
This is a Vitest/JS test file — the Go
package <pkg>_testwhite-box rule does not apply. No concerns.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Code Review — bookshelf-wro8e (PR #1337)
CI status observed: JS Unit Tests: success (2m21s). Test Race, Coverage, Lint, Integration, E2E API all green. E2E Browser failing — this is the unrelated a11y flake being addressed in PR #1336, not this PR's concern. PR is
mergeable: True.Phase 1: Spec compliance
The bead goal is to eliminate the 3 flaky fixed-microtask-drain loops (
for i < 16) in the METRON LAZY DETAIL "triggers detail fetch", "includes provider=metron", and "includes external_id" tests by replacing them withvi.waitFor(). All three conversions are present and correct in the diff.Phase 2: Code quality findings
[MINOR] static/js/test/metadata_fetch_controller_comic.test.js:155 —
expect(detailCall).toBeDefined()is now redundant aftervi.waitForIn the first converted test (
clicking a metron candidate with external_id triggers detail fetch), thevi.waitForblock already throws iffoundis falsy (theif (!found) throwguard), and returns the URL string. The subsequentexpect(detailCall).toBeDefined()(line 155 on the branch) is therefore a tautology —detailCallcannot beundefinedat that point becausevi.waitForwould have timed out instead. The other two tests (toContain("provider=metron"),toContain("external_id=12345")) don't have this redundancy because they assert a specific value. Suggested fix: remove the standaloneexpect(detailCall).toBeDefined()assertion; thevi.waitForcondition is the real gate, or replace it with a meaningful content assertion (e.g.toContain("candidate-detail")).[MINOR] static/js/test/metadata_fetch_controller_comic.test.js:202-222 — Sibling "save body" tests in METRON LAZY DETAIL still use fixed-tick drains for the same card-click → detail-fetch sequence
The "save body includes matched_candidate_provider=metron" (line 198) and "save body includes matched_candidate_external_id=cv_id" (line 215) tests — also inside the
METRON LAZY DETAILdescribe block and using the samemountWithMetronDetailUrl()+ card-click pattern — retainfor (var i = 0; i < 8) await Promise.resolve()(post-click) andfor (var i = 0; i < 16) await Promise.resolve()(post-card-click) drains. These are the same kind of timing-fragile loops that motivated this bead. They weren't converted. They are candidates for the samevi.waitFortreatment and remain a latent flake source for the same runner-load scenario. (Not a blocker for this PR since the bead scope is the three named tests, but worth noting as a follow-up.)[MINOR] static/js/test/metadata_fetch_controller_comic.test.js:133 —
mountWithMetronDetailUrlhelper still has a fixed 8-tick drainThe mount helper itself at line 133 (
for (var i = 0; i < 8; i++) { await Promise.resolve(); }) is a fixed-tick drain that lets the Stimulus controller connect and load providers before the test body runs. This is the same pattern as the removed drains and would fail under the same runner-load conditions if the controller requires >8 microtask ticks to settle. That said, mount-helper drains are typically more stable (no user-action sequencing), and this is outside the stated bead scope.Summary
The three conversions are correct. The
vi.waitForconditions wait on the right observable (thefetchmock call history forcandidate-detail), not a trivially-true condition. No behavior coverage was lost — thevi.waitForthrow semantics are strictly stronger than asserting after a fixed drain (it keeps polling until the condition holds or times out, whereas the old drain could silently pass withdetailCall === undefined). No fake-timer/real-timer mismatch was introduced by this change. The only substantive issue is the redundant.toBeDefined()(MINOR), and two categories of un-converted sibling drains (MINOR).REVIEW VERDICT: 0 blocker, 0 major, 3 minor
35801e1fdc077680d4f2Fold-delta re-review — PR #1337 (bookshelf-wro8e)
Reviewing ONLY the three addressed minors committed in
077680don top of the previously-clearedf71cb03base.CI / Mergeability
successfailure(pre-existingmove_toastflake being fixed separately by PR #1339 — not this PR's concern)successTrueFinding-by-finding assessment
1.
mountWithMetronDetailUrlhelper — 8-tick drain →vi.waitFor(fetchButton enabled)The converted condition (
!btn || btn.disabled) is the correct observable: the helper exists solely to reach a state where the fetch button is enabled (providers have loaded), and that state is driven by a real DOM mutation the controller performs after its_loadProviders()fetch resolves.vi.waitForhere uses real timers (nouseFakeTimersin scope at this point) and polls a genuine DOM state change. No deadlock risk. Correct conversion.2. Save-body tests — fixed 8-tick + 16-tick drains →
vi.waitFor(candidate-card)+vi.waitFor(save-button)Both save-body tests (
matched_candidate_provider=metronandmatched_candidate_external_id=cv_id) now:vi.waitForon.candidate-cardbefore clicking it — correct, that's the real signal the candidates render completed.vi.waitForon.mf-modal-save-row .btnbefore installing fake timers — this is the key ordering: the comment at line 231 ("vi.waitFor uses real timers so it must complete … before fake timers are installed") is accurate.vi.waitForruns under real timers here; only after it resolves doesvi.useFakeTimers()go in. After the save click the 8-tick drain continues under fake timers (suppressing the 800mswindow.location.assign) thenvi.useRealTimers()restores before theexpect. TheafterEachcallsvi.clearAllTimers()+vi.restoreAllMocks()so timer state does not leak between tests.The save-body assertions themselves are unchanged:
expect(capturedBody.matched_candidate_provider).toBe("metron")andexpect(capturedBody.matched_candidate_external_id).toBe(567890). Coverage is fully preserved.3. Tautology fix —
expect(detailCall).toBeDefined()→expect(detailCall).toContain("candidate-detail")The
vi.waitForalready guaranteesfoundis truthy before returning it, so the originaltoBeDefined()was vacuously true. The replacement asserts the URL contains the string"candidate-detail", which is meaningful: it confirms the correct endpoint was called (not just that any fetch happened). Correct and stronger.4. Remaining fixed-tick drains in the file
The drains remaining in the file (e.g. lines 235, 273, 287, etc.) are all in:
click(saveBtn)is needed to flush thefetchmicrotask queue while fake timers are active;vi.waitForcannot be used here because fake timers are installed. Correct to leave as-is.No new drains were introduced in the METRON LAZY DETAIL block.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
077680d4f2a2a38a3092