Skip to content

test(web): give the InvoiceWorkspace queued-Issue tests real waitFor headroom (#3219) - #3284

Open
bdunncompany wants to merge 1 commit into
LanternOps:mainfrom
bdunncompany:fix/3219-invoiceworkspace-flake
Open

test(web): give the InvoiceWorkspace queued-Issue tests real waitFor headroom (#3219)#3284
bdunncompany wants to merge 1 commit into
LanternOps:mainfrom
bdunncompany:fix/3219-invoiceworkspace-flake

Conversation

@bdunncompany

Copy link
Copy Markdown
Collaborator

Fixes #3219. Also covers #3277, which is the same two tests — worth closing as a duplicate once this lands.

Why these two tests and no others

Three sightings in ten days, always the same pair, always green locally and on a same-commit rerun:

when test
#2925 2026-07-29 does NOT fire a queued Issue when the pending save fails
#3219 2026-08-07 holds a header Issue click…
#3277 2026-08-08 both, on #3264's CI

They are the only two in the file that assert the end of a multi-hop propagation chain: the PATCH promise settles → the editor reports saved/failed → the workspace clears savePending → the header un-gates → the queued Issue fires a fetch. Every hop is promise/render scheduling.

All three recorded failures sat at ~1038ms against Testing Library's 1000ms default waitFor timeout. A fraction over the default is the signature of a timeout, not a logic race — and it explains why a rerun on the identical commit passes.

Why not fake timers

That was your other suggested option and it's the wrong instrument here: nothing in this chain is timer-driven. The delay is promise microtasks plus React render scheduling, neither of which fake timers advance. They'd add machinery without touching the cause, and would need the manual resolvePatch/rejectPatch promises reworked around them.

Raised file-wide as you specified, rather than per-assertion, so a future test in this file inherits the headroom instead of rediscovering the flake. Restored in afterAll since configure is process-global.

What I verified — and what I couldn't

Could not reproduce the flake locally. The chain completes in ~30ms on my machine against a 1000ms budget, which is precisely why this only ever fails on loaded CI. I'm not going to claim a green local run as proof of a fix for a CI-only timing failure.

What I could prove is that the knob governs the right assertions rather than being inert — setting asyncUtilTimeout to 1ms fails exactly the two queued-Issue tests and leaves the other five passing:

✓ renders a draft as the editor…            ✓ keeps the draft editor MOUNTED…
✓ surfaces an error card…                   ✓ offers the cost/margin toggle…
✓ updates the header from "Draft invoice"…
× holds a header Issue click while the editor is dirty…
× does NOT fire a queued Issue when the pending save fails…

The real proof is CI staying green on these two over the next few weeks; if it recurs, the diagnosis is wrong and the next step is instrumenting the chain rather than raising the number again.

Scope note

No other web test file configures asyncUtilTimeout — the whole suite runs on the 1000ms default. I deliberately did not raise it globally: that would also slow every genuine failure to the new ceiling, and the blast radius is the entire web suite. Worth considering separately if this shape shows up in other files.

Gate

apps/web vitest — 538 files / 5102 tests / 0 failures · tsc --noEmit exit 0 · eslint clean.

…headroom (LanternOps#3219)

Three sightings in ten days — LanternOps#2925 (2026-07-29), LanternOps#3219, and LanternOps#3277 on LanternOps#3264's CI
two days later — always the same two tests in this file, always passing locally
and on a same-commit rerun.

They are the only two tests here that assert the END of a multi-hop propagation
chain: the PATCH promise settles, the editor reports saved/failed, the workspace
clears `savePending`, the header un-gates, and only then does the queued Issue
fire a fetch. Every hop is promise/render scheduling, so under CI load the whole
chain can outrun Testing Library's 1000ms default `waitFor` timeout. All three
recorded failures sat at ~1038ms, a fraction over that default, which is the
signature of a timeout rather than a logic race.

Fake timers were the other candidate and are the wrong instrument: nothing in
this chain is timer-driven. The delay is promise microtasks plus React render
scheduling, neither of which fake timers advance, so they would add machinery
without touching the cause.

Raised file-wide via `configure({ asyncUtilTimeout })` rather than per-assertion,
so a future test in this file inherits the headroom instead of rediscovering the
flake, and restored in `afterAll` because that setting is process-global.

Verification, stated plainly: I could NOT reproduce the flake locally — the chain
completes in ~30ms here against the 1000ms default, which is exactly why this
only ever fails on loaded CI. What I could prove is that the knob governs the
right assertions: setting `asyncUtilTimeout` to 1ms fails EXACTLY the two
queued-Issue tests and leaves the other five passing.

Scope note: no other web test file configures `asyncUtilTimeout`, so the whole
suite runs on the 1000ms default. Deliberately NOT raised globally — that would
also slow every genuine failure to its new ceiling. Worth considering separately
if this shape shows up elsewhere.

Gate: apps/web vitest 538 files / 5102 tests / 0 failures; tsc --noEmit exit 0;
eslint clean.
@bdunncompany

Copy link
Copy Markdown
Collaborator Author

Fourth sighting, today. Same test, on a PR that cannot possibly have caused it.

Test Web went red on #3306 — a PR whose entire diff is one API contract test file, touching nothing under apps/web:

FAIL src/components/billing/InvoiceWorkspace.test.tsx > InvoiceWorkspace
     > holds a header Issue click while the editor is dirty, then fires it when the save settles
Test Files  1 failed | 538 passed (539)

Run 31312800729, 12:25Z. That takes the table in the PR body to:

when test
#2925 2026-07-29 does NOT fire a queued Issue when the pending save fails
#3219 2026-08-07 holds a header Issue click…
#3277 2026-08-08 both, on #3264's CI
2026-08-09 holds a header Issue click…, on #3306

Sighting four also strengthens the diagnosis rather than just repeating it: an API-only diff has no mechanism to perturb this component, so whatever moved was the runner's scheduling, which is exactly the ~1038ms-against-a-1000ms-default timeout story in the PR body. A same-commit rerun is green again, as before.

The cost is no longer theoretical — it now reddens unrelated PRs and forces a rerun to clear them, which is the same tax #3018 charges on the Windows agent side.

No competing PR exists for this (checked), and #3219 and #3277 are both still open and unassigned.

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.

[Web] Flaky test: InvoiceWorkspace "holds a header Issue click…" intermittently fails on invoice-issue-saving-hint

1 participant