Skip to content

Fix Vitest Windows CI EPERM Hang - #2

Open
skoll43 wants to merge 4 commits into
mainfrom
fix/vitest-eperm-hang
Open

Fix Vitest Windows CI EPERM Hang#2
skoll43 wants to merge 4 commits into
mainfrom
fix/vitest-eperm-hang

Conversation

@skoll43

@skoll43 skoll43 commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Fix Vitest kill EPERM Hang on Windows (Leaky File Locks)

Problem

On Windows environments, the CI and local npm test runs frequently hang at the very end of the test suite during the Vitest worker teardown phase, throwing a kill EPERM or Timeout terminating forks worker error.

While the majority of the codebase correctly uses in-memory databases (:memory:) or explicit store.cleanup() hooks, several integration test files were instantiating file-backed SessionDB and ContentStore instances (better-sqlite3) locally within test() blocks and never closing them.

Because Vitest runs in a worker pool, these unclosed C++ native file locks persist at the OS level. When Vitest attempts to terminate the worker pool, Windows rejects the termination or directory cleanup due to the active locks, causing the deadlock.

Solution

Introduced a scope-safe teardown pattern to the 5 anomalous test files:

  • tests/core/search-project-filter.test.ts
  • tests/integration/omp-plugin.test.ts
  • tests/integration/commit-message-symmetry.test.ts
  • tests/integration/cross-project-attribution.test.ts
  • tests/integration/seed-parity-coverage.test.ts

Pattern:
Added a module-level tracking array (const activeDBs = []) to each file. Instances are pushed to the array upon creation, and an afterEach() hook iterates through the array to forcefully call .close() (and .cleanup()) on every instance before clearing the array.

Verification

  • Verified locally that running the leaky tests (e.g., search-project-filter.test.ts) in isolation now tears down instantly (~2 seconds) on Windows without hanging.
  • The afterEach hook guarantees closure even if the test fails or times out.

mksglu and others added 4 commits July 19, 2026 17:36
A churned org's bridge kept firing a POST on every single event forever —
the 402 ("Subscription required") response fell through the status handling
(only 401 and 429 were handled) and the fire-and-forget caller never backed
off.

Now a 402 persists `suppressed_until` (epoch ms, now + 24h) into
platform.json — the same config file the bridge already owns — and prints
one concise stderr note. maybeForward gates on the marker BEFORE any network
call; an expired marker is cleared and forwarding resumes automatically, and
any 2xx response clears a lingering marker for immediate resume after
reactivation. Marker survives restarts and is shared across concurrent
sessions via the config file; in-flight 402 bursts dedupe to a single write
and note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Billing enforcement belongs to the platform side only. The server
already rejects churned-org forwards with 402 before any handler or
D1 write runs; the OSS bridge must stay billing-agnostic.

This reverts commit c94e8fc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants