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
Observed behavior
- 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).
- That file's tests pass and log their output normally, including
console.*/stdout emitted from inside the Worker.
- After that file's output, the process produces no further logs of any kind — no next file starting, no error.
- The process never exits on its own. Only an external timeout (CI job timeout, watchdog, etc.) kills it, many minutes later.
- 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
Related
Summary
A Vitest run using
@cloudflare/vitest-pool-workerscan hang indefinitely partway through a multi-file run: a workerd-backed test file's assertions andconsole.*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 theMiniflare.dispose()can delay or skipworkerdtermination behind earlier cleanup awaits #15085 dispose-ordering fix)vitest: 4.1.10miniflare: 5.20260815.0-alphaObserved behavior
@cloudflare/vitest-pool-workers(e.g. callingfetch()against the Worker's default export handler).console.*/stdout emitted from inside the Worker.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_plannedbut still describes a real defect: ifdispose()is invoked more than once for the sameMiniflareinstance, 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, soworkerdtermination isn't blocked behind other independent cleanup. It doesn't address a seconddispose()call short-circuiting and skipping steps entirely. If anything in the pool's per-file or per-run teardown sequence can end up callingdispose()twice for the same instance, #15097's bug would still produce a silent hang even on 0.22.0.Ask
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.@cloudflare/vitest-pool-workerswheredispose()could be called twice for the sameMiniflareinstance (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 Miniflare#dispose() rejects with ERR_SERVER_NOT_RUNNING when called twice, skipping the rest of its cleanup #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
Miniflare.dispose()can delay or skipworkerdtermination behind earlier cleanup awaits #15085 (fixed, included in 0.22.0) — different phase of teardown (final exit ordering)blockConcurrencyWhile+ console + throw)