Skip to content

test: wait for a timed-out body before cleanup, and retry removal where Windows retries - #561

Merged
mrgoonie merged 5 commits into
mainfrom
test/535-windows-runtime-suite-load
Oct 7, 2026
Merged

mrgoonie merged 5 commits into
mainfrom
test/535-windows-runtime-suite-load

Conversation

@mrgoonie

@mrgoonie mrgoonie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Refs #535

Cause (measured on Windows 11, 28 cores, Node 24.11, git 2.55, Playwright 1.63 Chromium)

Two separate defects produce the failures in #535.

1. The EPERM cleanup never retried

Every suite in #535 removes its temp directory with rmSync(dir, { maxRetries: 10, ... }), the #363 pattern. Under Node 24 on Windows, rmSync ignores maxRetries for a locked file: it throws EPERM at once. The promise form fs.promises.rm with the same options does retry.

I tested this with a directory held as the working directory of a child process that exits after 1.5 s:

Call Result
rmSync(dir, { recursive, force, maxRetries: 10, retryDelay: 100 }) EPERM after 0 ms
fs.promises.rm(dir, { same options }) removed after 1546 ms, once the child exited

That explains the issue comment that saw EPERM "even with maxRetries: 10". In practice the suites never retried. A loop that launched Chromium, await context.close()d it and then ran rmSync with retries hit EPERM on its first pass while the full suite was running. With fs.promises.rm, the same loop needed up to 1.6 s of retries and always succeeded. package-lifecycle-route.spec.ts had no retry at all.

2. The timeouts are spawn and browser-close latency under parallel load

I measured each operation alone and during a full vitest run, with temporary timing around the driver's launch/close and every git spawn (removed again):

Operation Alone (p50 / max) Full suite running (p50 / p90 / max, two runs)
git spawn, sync (test helpers) 37 / 162 ms 119–271 / 216–702 / 368–1664 ms
git spawn, async (managed-worktree.ts) 42 / 201 ms 152–343 / 304–779 / 1536–1924 ms
Chromium launch 116 / 168 ms 251–488 / 439–728 / 562–1516 ms
Chromium context.close() 36 / 154 ms 578–1226 / 4336–5060 / 6227–11789 ms

Each worktree test spawns git dozens of times (repositories, worktrees, then a sweep that asks git about each one), so a 3 s test reached 21.3 s (worktree-sweep) and 18.8 s (managed-worktree). Each browser test starts and stops a Chromium, and closing it dominates. Tests under 1 s alone took up to 10 s (driver, injection), and session-preview-real (two browsers in one test) took up to 14 s. The default budget is 20 s.

3. Cleanup raced the timed-out body

After a timeout, Vitest runs the hooks while the body is still awaiting git or the browser. The hooks then close the node's database and remove the directory under it, which is the EPERM in the reports.

What changed

  • tools/test-cleanup.ts (new; tests already import from tools/, see apps/desktop/test/appearance-tokens.spec.ts):
    • removeTestDirectory(path): fs.promises.rm with 10 retries at a growing delay (about 5.5 s at most), so the retry actually runs on Windows.
    • trackedTests(budgetMs): returns an it that records each body it starts, and settled(), which waits for all of them. Each suite's afterEach/afterAll awaits settled() before it closes anything or removes a directory, so cleanup never runs under a still-running body. The timed-out test still fails with Vitest's timeout error.
  • Budgets, scoped to the files whose tests drive git or Chromium and justified in a comment beside each:
    • worktree-sweep and managed-worktree: 60 s (worst measured 21.3 s and 18.8 s).
    • driver, injection and session-preview-real: 60 s (worst 10 s, 6 s and 14 s, where one close took up to 12 s). This is the budget click-answer.spec.ts already gives its browser tests.
    • click-answer (60 s) and submit-once (90 s) already carried these budgets per test, so they get only settled() and the retrying removal.
  • package-lifecycle-route.spec.ts: removal with retries. Its tests stay far below 20 s (worst 7.6 s), so its budget is unchanged.

The global testTimeout/hookTimeout and every assertion are unchanged. No product code changed.

Evidence

Stress, before (origin/main fce8dd0) vs after, alternated. Each round ran the full vitest run plus three concurrent runs of the 8 suites, all at once. That is heavier than pnpm verify.

Round Before: processes failing (failed tests) Before: EPERM After: processes failing (failed tests) After: EPERM
1 3/4 (6) 1 0/4 0
2 3/4 (4) 1 4/4 (8, see below) 0
3 4/4 (39) 4 0/4 0
Total 10/12 (49) 6 4/12 (8) 0

After round 2: all four processes slowed down together in one two-minute window, with other work also running on the machine. Every browser test in that window ran 5–50× slower, and many passed at 40–58 s. Seven tests ran out of the 60 s budget, click-answer included, whose 60 s budget is unchanged from main. The eighth failure was an unrelated timing assertion in scoped-fs.spec.ts (expected 2000 to be less than 2000). In one process an afterAll waiting on settled() hit the 20 s hook timeout. That is the intended trade: a slow body is waited for, and the directory is never removed under it. None of these failures was EPERM.

Proof that cleanup waits for a timed-out body. A throwaway spec (not committed) used a 200 ms budget and a body that runs for 1.5 s. Vitest reported Test timed out in 200ms. afterAll saw finished=false before settled() and finished=true after it, having waited 1294 ms.

Normal load. Full vitest run on main: 5 runs, 0 failures. Under that load the target tests took up to 21.3 s, at or over the old 20 s budget. On this branch: full runs in the stress rounds above, and pnpm verify plus two more full runs: corepack pnpm verify exited 0 (13/13 invariants, typecheck, lint, 524 files passed and 1 skipped, 6977 tests passed). The two full runs passed 7015/7015 with 0 failures. In those runs session-preview-real again took 19.3 s and worktree-sweep 12.4 s, so the old 20 s budget had almost no headroom.

  • 8 changed suites alone: 8 files, 74 tests passed.
  • corepack pnpm typecheck: passed.
  • corepack pnpm invariants: all 13 passed.
  • eslint on the changed files: clean.

Not changed here

Docs impact

None. Test infrastructure only.

…re Windows retries

Under Node 24 on Windows, rmSync ignores maxRetries for a locked file and throws EPERM at once, so the cleanup
retries in the git and browser suites never ran. Remove their temp directories with fs.promises.rm, which does retry.

Vitest runs cleanup hooks while a timed-out body is still running git or a browser. These suites now track their
test bodies and wait for them before closing the node or removing a directory.

Git spawns and Chromium closes slow down 4 to 30 times while the full suite runs. The suites whose tests drive them
get a 60 s budget, sized from the measured worst cases and documented beside it.
@mrgoonie

mrgoonie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Review attestation: ready to merge at 0f379a3050f5b1b977bd7c806fa6e34133b7c8f9, reviewed by agent:code-reviewer.

A push to this PR makes this attestation stale; the new head needs its own review.

@mrgoonie
mrgoonie enabled auto-merge (squash) October 7, 2026 14:24

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@mrgoonie

mrgoonie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Review attestation: ready to merge at cdd6d352b1cd49c7f31a253eff529ec127734151, reviewed by agent:code-reviewer.

A push to this PR makes this attestation stale; the new head needs its own review.

@mrgoonie

mrgoonie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Review attestation: ready to merge at 7b2f66b74e58d2eb09ae5d15cda0480a14d3d01e, reviewed by agent:code-reviewer.

A push to this PR makes this attestation stale; the new head needs its own review.

@mrgoonie

mrgoonie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Review attestation: ready to merge at 6c949921f7dc7ba1d9c4367a48e3c72cfbab2178, reviewed by agent:code-reviewer.

A push to this PR makes this attestation stale; the new head needs its own review.

@mrgoonie

mrgoonie commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Review attestation: ready to merge at 1b3f775e0d91265bc3118355b972339999cffbc0, reviewed by agent:code-reviewer.

A push to this PR makes this attestation stale; the new head needs its own review.

@mrgoonie
mrgoonie merged commit 317c004 into main Oct 7, 2026
35 of 41 checks passed
@mrgoonie
mrgoonie deleted the test/535-windows-runtime-suite-load branch October 7, 2026 20:04
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.

1 participant