cloudflare / cloudflare/workers-sdk

Vitest run stalls indefinitely mid-suite after a workerd pool test file finishes, with no error output

Open
#15,498 1 comment 0 reactions 0 assignees View on GitHub
package:vitest
Dominant language
TypeScript
Stars
4.5k
Forks
1.5k
Avg merge
3d 8h
Merged PRs (30d)
186

Description

### Summary

A Vitest run using `@cloudflare/vitest-pool-workers` can hang indefinitely partway through a multi-file run: a workerd-backed test file's assertions and `console.*` output all print successfully, and then the run produces zero further output — no next test file starting, no completion summary, no error, no stack trace — until an external watchdog/timeout kills the process. This looks distinct from #15085 (fixed in 0.22.0) and #14180 (fixed): those hangs happen at *final* process teardown; this one happens *mid-run*, before the suite has even finished running all test files.

### Versions

- `@cloudflare/vitest-pool-workers`: 0.22.0 (already includes the #15085 dispose-ordering fix)
- `vitest`: 4.1.10
- `miniflare`: 5.20260815.0-alpha
- Node.js: 22.x
- Linux (containerized CI runner)

### Observed behavior

1. A Vitest run includes several test files; at least one exercises a Worker via `@cloudflare/vitest-pool-workers` (e.g. calling `fetch()` against the Worker's default export handler).
2. That file's tests pass and log their output normally, including `console.*`/stdout emitted from inside the Worker.
3. After that file's output, the process produces no further logs of any kind — no next file starting, no error.
4. The process never exits on its own. Only an external timeout (CI job timeout, watchdog, etc.) kills it, many minutes later.
5. Reproduced twice in a row under the same conditions (same file, same batch of files run together), so it doesn't look like a one-off flake.

This is easiest to hit when several test files run together in one long-lived process (either Vitest's normal multi-file run, or a sharding/batching tool that groups files into one process) — the hang isn't necessarily tied to being the *last* file in the run, just to a workerd-backed file's pool being torn down for one file while other files are still pending in the same run.

### Suspected root cause

This matches the failure mode described in #15097 ("Miniflare#dispose() rejects with ERR_SERVER_NOT_RUNNING when called twice, skipping the rest of its cleanup"), which was closed `not_planned` but still describes a real defect: if `dispose()` is invoked more than once for the same `Miniflare` instance, the second call rejects early on an already-closed server and **skips the remaining cleanup steps** — including steps that stop things keeping the Node event loop alive (e.g. the dev registry file watcher, per that issue's own description). A leaked handle from a skipped cleanup step would explain a process that just sits there indefinitely with no output and no error, rather than crashing or exiting cleanly — which is exactly what's observed here.

#15085 fixed the *ordering* within a single `dispose()` call, so `workerd` termination isn't blocked behind other independent cleanup. It doesn't address a *second* `dispose()` call short-circuiting and skipping steps entirely. If anything in the pool's per-file or per-run teardown sequence can end up calling `dispose()` twice for the same instance, #15097's bug would still produce a silent hang even on 0.22.0.

### Ask

- Could #15097 be reconsidered? Making `dispose()` idempotent (treat "already stopped" as success and continue the remaining cleanup, rather than throwing and skipping the rest) looks like it closes a real gap regardless of how contrived that issue's specific repro was.
- If there's a known code path in `@cloudflare/vitest-pool-workers` where `dispose()` could be called twice for the same `Miniflare` instance (e.g. a per-file teardown racing a later run-level teardown), a pointer there would help narrow this down. I don't have an isolated minimal repro yet — this was observed under real multi-file CI runs, not a synthetic minimal case — but the shape matches #15097 closely enough that I suspect it's the same underlying issue resurfacing as a mid-run hang instead of a final-exit hang.

### Related

- #15085 (fixed, included in 0.22.0) — different phase of teardown (final exit ordering)
- #14180 (fixed) — different trigger (DO `blockConcurrencyWhile` + console + throw)
- #15097 (closed not_planned) — likely root cause of this issue

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the multi-file run with @cloudflare/vitest-pool-workers and a Worker default export handler, then inspect the Miniflare dispose() path and compare it with #15097. Trace whether the same Miniflare instance can be disposed twice during per-file and run-level teardown. Done means cleanup completes without leaked handles and the Vitest process advances to the remaining files and exits normally.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
backend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.