Summary
When running mise test:init, the dev server processes spawned during hydration tests (specifically tsx watch and its child processes like dotenvx) are not fully terminated after each test case completes. Over the course of a full test run (560+ cases), orphaned processes accumulate and gradually slow down the system.
Problem
The current implementation in packages/init/src/test/server.ts uses @david/dax to spawn the dev server process. When the test completes, it calls CommandChild.kill("SIGKILL") to terminate the process. However, this only kills the direct child process (npm/pnpm), not the entire process tree.
The typical process tree for frameworks using tsx watch (bare-bones, express, hono, elysia) looks like:
dax shell → npm/pnpm → sh → dotenvx → tsx watch → node app
Killing only the top-level dax process leaves dotenvx, tsx watch, and the node application running as orphans.
Root Cause
@david/dax executes commands through its own JS-based shell (deno_task_shell), which does not expose process PIDs or support process group management. The CommandChild.kill() method sends a signal through dax's internal KillSignal abstraction, which cannot propagate to the full OS process tree.
This is a known limitation — see dsherret/dax#351, where the maintainer confirmed that PID exposure is not straightforward due to dax's architecture.
Solution
Replace @david/dax process spawning with node:child_process.spawn() using detached: true to create a new process group. On cleanup, use process.kill(-child.pid, 'SIGKILL') to terminate the entire process group, ensuring all descendant processes (including tsx watch, dotenvx, etc.) are killed.
Key changes in packages/init/src/test/server.ts:
- Replace
@david/dax $ spawn with child_process.spawn({ detached: true })
- Use
Readable.toWeb() to convert Node.js streams to Web ReadableStream (for .tee() compatibility)
- Kill entire process group via
process.kill(-pid, 'SIGKILL') in cleanup
Related
Summary
When running
mise test:init, the dev server processes spawned during hydration tests (specificallytsx watchand its child processes likedotenvx) are not fully terminated after each test case completes. Over the course of a full test run (560+ cases), orphaned processes accumulate and gradually slow down the system.Problem
The current implementation in
packages/init/src/test/server.tsuses@david/daxto spawn the dev server process. When the test completes, it callsCommandChild.kill("SIGKILL")to terminate the process. However, this only kills the direct child process (npm/pnpm), not the entire process tree.The typical process tree for frameworks using
tsx watch(bare-bones, express, hono, elysia) looks like:Killing only the top-level dax process leaves
dotenvx,tsx watch, and the node application running as orphans.Root Cause
@david/daxexecutes commands through its own JS-based shell (deno_task_shell), which does not expose process PIDs or support process group management. TheCommandChild.kill()method sends a signal through dax's internalKillSignalabstraction, which cannot propagate to the full OS process tree.This is a known limitation — see dsherret/dax#351, where the maintainer confirmed that PID exposure is not straightforward due to dax's architecture.
Solution
Replace
@david/daxprocess spawning withnode:child_process.spawn()usingdetached: trueto create a new process group. On cleanup, useprocess.kill(-child.pid, 'SIGKILL')to terminate the entire process group, ensuring all descendant processes (includingtsx watch,dotenvx, etc.) are killed.Key changes in
packages/init/src/test/server.ts:@david/dax$spawn withchild_process.spawn({ detached: true })Readable.toWeb()to convert Node.js streams to WebReadableStream(for.tee()compatibility)process.kill(-pid, 'SIGKILL')in cleanupRelated
.spawn()(open)mise test:init#648 — Improvemise test:init