test(512): flush LibrariesScreen scan-poll chain deterministically instead of waitFor timeouts #558

Merged
timothy merged 1 commits from issue-512-deterministic-libraries-test into main 2026-07-22 18:02:25 +02:00
Owner

Fixes #512.

What

web/src/screens/LibrariesScreen.test.tsx → "stops scan polling and refreshes sources once when scans complete" was CI-load-flaky. It fired the poll tick via runPollTick but discarded the promise loadScanStatuses returns, so the progress-clear state update and the fire-and-forget loadSources() refetch it triggers landed on real microtasks after act() had already resolved. The test out-waited that race with two waitFor(…, { timeout: 5000 }) calls, which were marginal on a starved CI VM (timed out on PR #509 / run 918); prior timeout bumps (#447) were diminishing whack-a-mole.

Fix

Drive the chain to completion deterministically instead of out-waiting it. runPollTick now, inside act(async () => …):

  1. await Promise.all(intervalHandlers.map(h => h())) — settles the scan-status poll promise the handler returns (and its progress-clear setState);
  2. await flushMicrotasks() (a single setTimeout(0) macrotask) — drains the entire microtask queue, including the fire-and-forget loadSources() refetch, which by spec has fully settled before any macrotask runs.

The mock fetches resolve synchronously on microtasks and setInterval is the only mocked timer (setTimeout stays real), so one macrotask turn deterministically completes both chains with no wall-clock delay. Both original assertions are preserved verbatim — progress label 75% gone, and exactly one extra /api/v1/media-sources fetch — and the two waitFor calls + the 20s test-level budget are dropped.

Verification

  • LibrariesScreen suite green 15/15 consecutive runs (npm test -- LibrariesScreen --run in a loop).
  • npm run lint clean; npm run build succeeds (only the pre-existing unrelated chunk-size advisory).
  • No production code changed — LibrariesScreen.tsx / libraries.ts untouched; the component was observable deterministically from the harness alone.

Done-when

  • The scan-complete test no longer depends on waitFor wall-clock timeouts to observe the poll chain
  • Test passes reliably (verified with a repeated-run loop locally — 15/15)
  • adversarial review passed

Pure web-test change — no server-state effect (independent cross-model review skipped per process.independent-review-rubric; covered by orchestrator cold-read of the diff + the 15/15 determinism loop).

🤖 Generated with Claude Code

Fixes #512. ## What `web/src/screens/LibrariesScreen.test.tsx` → *"stops scan polling and refreshes sources once when scans complete"* was CI-load-flaky. It fired the poll tick via `runPollTick` but **discarded the promise `loadScanStatuses` returns**, so the progress-clear state update and the fire-and-forget `loadSources()` refetch it triggers landed on real microtasks **after** `act()` had already resolved. The test out-waited that race with two `waitFor(…, { timeout: 5000 })` calls, which were marginal on a starved CI VM (timed out on PR #509 / run 918); prior timeout bumps (#447) were diminishing whack-a-mole. ## Fix Drive the chain to completion **deterministically** instead of out-waiting it. `runPollTick` now, inside `act(async () => …)`: 1. `await Promise.all(intervalHandlers.map(h => h()))` — settles the scan-status poll promise the handler returns (and its progress-clear `setState`); 2. `await flushMicrotasks()` (a single `setTimeout(0)` macrotask) — drains the entire microtask queue, including the fire-and-forget `loadSources()` refetch, which by spec has fully settled before any macrotask runs. The mock fetches resolve synchronously on microtasks and `setInterval` is the only mocked timer (`setTimeout` stays real), so one macrotask turn deterministically completes both chains with no wall-clock delay. Both original assertions are **preserved verbatim** — progress label `75%` gone, and **exactly one** extra `/api/v1/media-sources` fetch — and the two `waitFor` calls + the 20s test-level budget are dropped. ## Verification - `LibrariesScreen` suite green **15/15 consecutive runs** (`npm test -- LibrariesScreen --run` in a loop). - `npm run lint` clean; `npm run build` succeeds (only the pre-existing unrelated chunk-size advisory). - **No production code changed** — `LibrariesScreen.tsx` / `libraries.ts` untouched; the component was observable deterministically from the harness alone. ## Done-when - [x] The scan-complete test no longer depends on `waitFor` wall-clock timeouts to observe the poll chain - [x] Test passes reliably (verified with a repeated-run loop locally — 15/15) - [ ] adversarial review passed Pure web-test change — no server-state effect (independent cross-model review skipped per `process.independent-review-rubric`; covered by orchestrator cold-read of the diff + the 15/15 determinism loop). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 1 commit 2026-07-22 17:04:31 +02:00
test(512): flush LibrariesScreen scan-poll chain deterministically instead of waitFor timeouts
PR Gates / Docs update reminder (pull_request) Successful in 18s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 18s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 8s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m57s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 20m49s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m9s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
b5596f55f3
The "stops scan polling and refreshes sources once when scans complete" test
fired the poll tick via runPollTick but discarded the promise loadScanStatuses
returns, so the progress-clear state update and the fire-and-forget
loadSources() refetch it triggers landed on real microtasks AFTER act() had
resolved. The test out-waited that race with two waitFor({ timeout: 5000 })
calls, which were marginal on a starved CI VM (timed out on PR #509/run 918);
prior timeout bumps (#447) were diminishing whack-a-mole.

Drive the chain to completion deterministically instead of out-waiting it:
runPollTick now awaits the promise the handler returns (settling the
progress-clear) and then yields to a single macrotask (setTimeout 0) to drain
the microtask queue — including the fire-and-forget loadSources() refetch —
all INSIDE act(async () => ...). The mock fetches resolve synchronously on
microtasks and setInterval is the only mocked timer, so one macrotask turn
completes both chains with no wall-clock delay. The two waitFor calls are
replaced by synchronous assertions (progress label gone; exactly one extra
/api/v1/media-sources fetch), and the 20s test-level budget is dropped.

Verified: LibrariesScreen suite green 15/15 consecutive runs; npm run lint and
npm run build clean. No production code changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Fixes #512
Author
Owner

Adversarial review (orchestrator cold-read of the diff + determinism loop). Pure web-test leaf, no server-state effect → cross-model independent review skipped per process.independent-review-rubric, stated here. Checks: both original assertions preserved verbatim (progress 75% gone; exactly before + 1 /media-sources fetch) — not weakened; runPollTick awaits the handler promises then drains one setTimeout(0) macrotask inside act(), which by spec settles the whole microtask queue (incl. the fire-and-forget loadSources() refetch) before the macrotask fires — race-free with no wall-clock wait; setInterval is the only mocked timer so setTimeout(0) stays real; no production code touched. 15/15 consecutive loop runs green; lint + build clean.

Review-verdict: MERGEABLE @ b5596f55

Adversarial review (orchestrator cold-read of the diff + determinism loop). Pure web-test leaf, no server-state effect → cross-model independent review skipped per `process.independent-review-rubric`, stated here. Checks: both original assertions preserved verbatim (progress `75%` gone; exactly `before + 1` `/media-sources` fetch) — not weakened; `runPollTick` awaits the handler promises then drains one `setTimeout(0)` macrotask inside `act()`, which by spec settles the whole microtask queue (incl. the fire-and-forget `loadSources()` refetch) before the macrotask fires — race-free with no wall-clock wait; `setInterval` is the only mocked timer so `setTimeout(0)` stays real; no production code touched. 15/15 consecutive loop runs green; lint + build clean. Review-verdict: MERGEABLE @ b5596f55
Author
Owner

⚠️ CI reds on this PR are infra, not code — the runner host hit No space left on device during the build step. Tracked in server-management#654. On the last run, Functional E2E + all PR-Gates went green here on b5596f5; only the disk-starved EF-migration/Build jobs fast-failed. Merge is pending a clean rerun once the runner-host capacity issue is resolved — no code change needed.

⚠️ CI reds on this PR are **infra, not code** — the runner host hit `No space left on device` during the build step. Tracked in **server-management#654**. On the last run, Functional E2E + all PR-Gates went green here on `b5596f5`; only the disk-starved EF-migration/Build jobs fast-failed. Merge is pending a clean rerun once the runner-host capacity issue is resolved — no code change needed.
timothy merged commit d49d078264 into main 2026-07-22 18:02:25 +02:00
timothy deleted branch issue-512-deterministic-libraries-test 2026-07-22 18:02:28 +02:00
Sign in to join this conversation.