Vitest flake (recurring, 2 PRs): ProjectRoadmapCanvas.test.tsx:128 — the auto-reset assertion races the auto-layout effect instead of awaiting it
Symptom
A Vitest shard fails on tests/components/ProjectRoadmapCanvas.test.tsx > ProjectRoadmapCanvas > "auto-resets a level when its auto-laid node set changes (a re-plan)":
AssertionError: expected "vi.fn()" to be called with arguments: [ ArrayContaining ["A", "C"] ]
Number of calls: 0
❯ tests/components/ProjectRoadmapCanvas.test.tsx:128:30
1 failed / 195 passed test files. Passes 25/25 locally, re-run 3× on the exact failing commit. gh run rerun --failed clears it.
Recurrence (diff-unrelated every time — the tell it's the test, not the diff)
- 2026-07-07, PR #1560 —
Vitest (integration + coverage). - 2026-07-27, PR #1620 —
Vitest (3/3), on an E2E-test-ONLY diff (tests/e2e/**, which Vitest does not even run). The failing file imports onlyProjectRoadmapCanvas+projectCanvasModel— nothing the diff touches. Every other check green, including theacceptance-videolane that actually runs the new spec.
Root cause
The test is a race, not a timing-margin problem — the classic shape of notes.html #37 / CLAUDE.md § E2E tests wait on the AUTHORITATIVE signal, here at the component altitude:
await screen.findByText('c'); // :127
expect(onResetPositions).toHaveBeenCalledWith(expect.arrayContaining(['A', 'C'])); // :128
The findByText('c') DOES resolve — so the level re-rendered with the new node set — but that is the wrong signal: it proves the render landed, not that the auto-layout effect that calls onResetPositions has flushed. expect(...).toHaveBeenCalledWith(...) is a synchronous, non-retrying assertion, so on a loaded CI runner it samples the mock before the effect fires and reads Number of calls: 0. Locally the effect wins the race every time.
A flaky Vitest test is not a private cost: PR CI checks out the branch merged with main, so it intermittently red-lights every open PR.
Fix
Wait on the authoritative signal — the callback itself — instead of a proxy for it:
await waitFor(() =>
expect(onResetPositions).toHaveBeenCalledWith(expect.arrayContaining(['A', 'C'])),
);
Then sweep the file for siblings of the same shape: any expect(someMock).toHaveBeenCalled… that follows a findBy*/fireEvent and asserts an effect-driven callback. A bigger timeout fixes nothing here — there is no timeout in the failing line.
Acceptance criteria
- The named test awaits
onResetPositionsauthoritatively (waitFor/expect.poll), not a render proxy. - Every other assertion in
ProjectRoadmapCanvas.test.tsxthat asserts an effect-driven mock after a render is converted the same way (or shown to already await one). - The file passes 25/25 under repeat load locally (e.g.
vitest run --repeat 5, or run under CPU contention), not just once. - No production component change — this is a test-side race.
Notes
Logged in the auto-flake-log across both occurrences; filed at Yue's request rather than waiting for a 3rd. Same family as MOTIR-1699 (bulk-4 waitForResponse) and MOTIR-1679 (bulk-shard webServer), and the same lesson as MOTIR-671 (bug-e2e-suite-flaky-specs) one altitude down. Surfaced while shipping MOTIR-1733 (PR #1620), whose own diff is innocent.