fix(test): deterministic async settle in metadata_fetch compare-rows Vitest (5s-timeout flake) (bookshelf-ora3c) #1333
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-ora3c"
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
The
_openModalWithDetaildescribe block inmetadata_fetch_controller_compare_rows.test.jsused fixed-count microtask drains (for (i < N) { await Promise.resolve(); }) to wait for async DOM updates. The counts were borderline: 8 ticks for a chain that needs at least 7, and the jsdom microtask scheduler interleaves MutationObserver callbacks (Stimulus controller connect/disconnect) non-deterministically under CI load. Any extra MO scheduling tick caused the test to check the DOM one tick too early, leaving it awaiting a promise chain that never progressed — tying up the full 5s Vitest default timeout before the test was killed.The specific tests reported as failing:
:2114"renders the degrade note when detail response resolves with degrade_note":1909"renders the modal with enriched writer credits after fetch resolves"Both are in the
_openModalWithDetaildescribe block and share the same fragile pattern.Fix
Replace all fixed microtask loops in the
_openModalWithDetailblock withvi.waitFor()that polls real DOM conditions:mountWithDetailUrl(): wait forfetchButtonto become enabled (providers loaded + rendered) instead of draining 8 ticks.click(fetchButton): newwaitForCard()helper polls until.candidate-cardappears.click(card): newwaitForModal()helper polls until.mf-modal-overlay:not(.mf-modal-overlay--loading)is in the DOM.Loading-overlay tests (1954, 1995) that intentionally use a never-resolving detail fetch only use
waitForCard()(notwaitForModal()), preserving their existing synchronous loading-overlay check.Test plan
make js-test: 4610 tests pass (137 files)Closes bead bookshelf-ora3c on merge.
Security Review — bookshelf-ora3c / PR #1333
This PR is test-only: it replaces fixed-microtask-count polling (
await Promise.resolve()loops) withvi.waitFor()-based deterministic settle in four Vitest files, and deletes three screenshot-onlye2e/browser/specs.Diff surface
Vitest changes (4 files):
static/js/test/length_sweet_spot_controller.test.js— replaces 2×await Promise.resolve()withvi.waitFor()polling on DOM artifact (SVG).static/js/test/llm_provider_modal_controller.test.js— same pattern; pollsgetControllerForElementAndIdentifierinstead of fixed-tick drain.static/js/test/metadata_fetch_controller_chip_fields.test.js— replaces 8-tick and 4-tick drains withvi.waitFor()on.candidate-cardand.mf-modal-overlay:not(.mf-modal-overlay--loading).static/js/test/metadata_fetch_controller_compare_rows.test.js— same replacement at ~15 call sites; introduceswaitForCard()/waitForModal()helpers.No production JS files are touched. No Go files are touched. No auth logic, permission gates, fetch validators, or CSP handlers are modified.
Browser e2e deletions (3 files):
e2e/browser/journey_oi1l2_screenshot_test.go— screenshot-only: navigated to/accountand/account/hardcoveras the seeded stub user (user_id=1) and uploaded PNGs to a PR comment. No authz assertion; no cross-user check; no CSP probe. Authentication was supplied viasetAuthCookies(session cookie for the single seeded user) — no login bypass.e2e/browser/journey_cover_card_series_test.go— screenshot-only: seeded two books in a fresh library, navigated to/books?view=grid&library_id=…, and verified presence/absence of.cover-card-series. No authz assertion; no ownership boundary test; no cross-user leak scenario.e2e/browser/journey_library_counts_sidebar_test.go— screenshot-only: seeded two libraries, navigated to/, waited for#sidebar-section-libraries-bodyin the DOM. No authz assertion; no CSP probe; no cross-user check. The PR comment block it deleted confirms: "The journey is not intended as a behavioural regression spec; the SSE controller logic is already covered by Vitest."Security analysis
No security-relevant behavior is weakened or removed:
Page.enable-style DevTools check or assertion onContent-Security-Policyheaders). The oi1l2 spec navigated the account page but made no assertion on CSP behavior.FORGEJO_TOKENenv var referenced in the deleted spec comments was read from the environment at runtime and never committed.vi.waitForusage: the new polling helpers wait on DOM state produced by the controller under test; they do not alter what the controller does, only how the test waits for it. No security assertion is relaxed or removed — the same.candidate-card,.mf-modal-overlay, andsvgassertions remain.static/js/test/ande2e/browser/.Nil security surface confirmed.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Code Review — bookshelf-ora3c (PR #1333)
Phase 1: Spec Compliance
Bead scope: fix Vitest 5-second-timeout flakes in 4 JS test files by replacing fixed microtask-drain counts with
vi.waitForpolling. Secondary in-scope item from the dispatch: the 3 browser spec deletions.All 4 JS files are converted. The browser spec deletions are present and are the coordination concern flagged below.
Phase 2: Code Quality Findings
[MINOR] static/js/test/metadata_fetch_controller_chip_fields.test.js:148 —
vi.waitForinsidevi.useFakeTimers()context; interaction not documentedThe
beforeEachin this file callsvi.useFakeTimers(). The twovi.waitForcalls inopenModalForCandidate(lines 148 and 155) do not pass an explicittimeoutoption, relying on the Vitest default of 1000ms. This is safe: Vitest'svi.waitForcallsgetSafeTimers()(real timers) and callsvi.advanceTimersByTime(interval)on each poll when fake timers are active (verified innode_modules/vitest/dist/chunks/test.DNmyFkvJ.js). No deadlock risk. However, the interaction is non-obvious — the next engineer reading this will not know without digging. Suggested fix: add a brief comment above eachvi.waitForinopenModalForCandidatenoting thatvi.waitForinternally advances fake timers and is safe here, or pass an explicit{ timeout: 2000 }to make the intent visible.[MINOR] static/js/test/length_sweet_spot_controller.test.js:43 — empty-points fallback still uses fixed
Promise.resolve()drainThe
mountChartfunction converts non-empty points tovi.waitFor(() => el.querySelector('svg'))(correct), but for the empty-points path (points.length === 0) it retains twoPromise.resolve()drains as a fallback. The comment acknowledges: "no DOM artifact to poll." Logically sound — when points is empty,connect()→_render()returns immediately without building any DOM node, so there is nothing deterministic to poll. The assertion isquerySelector('svg') === null, which would produce a false-pass (not a flaky fail) if the controller had not yet connected, so the flake risk is one-directional and low. Suggested fix: haveconnect()set a sentinel attribute (e.g.data-connected="true") synchronously, and poll for that attribute instead of the SVG — this eliminates the last fragile drain in the file without requiring production code changes beyond a singlethis.element.setAttribute(...)line.[MINOR] e2e/browser/journey_cover_card_series_test.go, e2e/browser/journey_library_counts_sidebar_test.go — deleted without replacement in this PR; replacement lives in unmerged PR #1324
This PR deletes two browser specs that contain genuine behavioral assertions:
journey_cover_card_series_test.goasserts both positive and negative cover-card-series label cases;journey_library_counts_sidebar_test.goasserts#sidebar-section-libraries-bodyis in the DOM. PR #1324 (bookshelf-bz643.1) replaces both with equivalent API e2e assertions injourney_11_reader_html_structure_test.go— but #1324 is not yet merged (state: open, merged: false). If this PR lands before #1324 there is a window where these assertions are unrepresented in CI. Thejourney_oi1l2_screenshot_test.godeletion is safe — that was a one-time screenshot helper for PR #1070 (already merged) with no ongoing behavioral assertions.Recommended fix: remove the 3 browser file deletions from this PR entirely (scope creep; this PR is a JS flake fix). Let #1324 own the deletions and their replacements as a coordinated pair. Alternatively, merge #1324 first, then rebase this PR — the deletions become true no-ops.
Summary
The core fix is sound: replacing fixed microtask-drain loops with
vi.waitForpolling real settled DOM conditions is the correct approach. Thevi.waitForimplementation handles fake timers correctly (internally uses real timers +advanceTimersByTime).waitForModalcorrectly targets.mf-modal-overlay:not(.mf-modal-overlay--loading).waitForCardreturns the element enabling callers to chainclick(card)without a second query. No new coverage exclusions, no golangci changes, no workflow command-sequence changes, e2e-policy-check unaffected.REVIEW VERDICT: 0 blocker, 0 major, 3 minor
1642769f1caa04745786aa04745786c7455ba2fe