chore(test): add cross-worktree test lock and clean up dev-server test child

- scripts/test-with-lock.mjs: pnpm test:locked acquires an exclusive
  ~/.fusion/test.lock (O_EXLOCK) before running pnpm test, so several
  Claude Code worktrees on the same machine serialize their vitest
  fan-out instead of saturating the box. Prints the holding PID and
  worktree path while waiting.
- dev-server-manager test children now park on stdin instead of
  setInterval so manager.shutdown() can deterministically exit them
  via stdin close, preventing orphaned node processes outliving the
  test run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-02 15:56:46 -07:00
parent 2f13a31e13
commit 1e6714e4df
5 changed files with 206 additions and 31 deletions

View File

@@ -30,7 +30,7 @@ async function waitFor(predicate: () => boolean, timeoutMs = 4_000): Promise<voi
}
function longRunningCommand(previewUrl = "http://127.0.0.1:4173/preview"): DevServerStartOptions {
const script = `console.log('ready ${previewUrl}'); setInterval(() => {}, 1000);`;
const script = `console.log('ready ${previewUrl}');process.stdin.resume();process.stdin.on('end',()=>process.exit(0));`;
return {
command: `node -e \"${script}\"`,
scriptName: "dev",

View File

@@ -60,7 +60,7 @@ describe("DevServerManager", () => {
});
await manager.start({
command: "node -e \"console.log('preview at http://localhost:5173/'); setInterval(() => {}, 1000)\"",
command: "node -e \"console.log('preview at http://localhost:5173/');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
scriptName: "dev",
});
@@ -82,7 +82,7 @@ describe("DevServerManager", () => {
await store.updateState({ manualUrl: "https://localhost:9999" });
await manager.start({
command: "node -e \"console.log('ready at http://localhost:4321/'); setInterval(() => {}, 1000)\"",
command: "node -e \"console.log('ready at http://localhost:4321/');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
scriptName: "dev",
});
@@ -106,7 +106,7 @@ describe("DevServerManager", () => {
managers.push(manager);
await manager.start({
command: "node -e \"setInterval(() => {}, 1000)\"",
command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
scriptName: "dev",
});
@@ -125,7 +125,7 @@ describe("DevServerManager", () => {
managers.push(manager);
await manager.start({
command: "node -e \"setInterval(() => {}, 1000)\"",
command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
scriptName: "dev",
});
@@ -141,7 +141,7 @@ describe("DevServerManager", () => {
managers.push(manager);
await manager.start({
command: "node -e \"setInterval(() => {}, 1000)\"",
command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
scriptName: "dev",
});
@@ -160,7 +160,7 @@ describe("DevServerManager", () => {
managers.push(manager);
await manager.start({
command: "node -e \"setInterval(() => {}, 1000)\"",
command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
scriptName: "dev",
});

View File

@@ -60,7 +60,7 @@ describe("DevServerProcessManager", () => {
it("start() spawns child process and updates state to running", async () => {
const { root, manager } = await createManager();
const state = await manager.start("node -e \"setInterval(() => {}, 1000)\"", root);
const state = await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
expect(state.status).toBe("running");
expect(typeof state.pid).toBe("number");
@@ -73,14 +73,14 @@ describe("DevServerProcessManager", () => {
manager.once("started", () => resolve());
});
await manager.start("node -e \"setInterval(() => {}, 1000)\"", root);
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
await startedEvent;
});
it("start() captures stdout into log buffer", async () => {
const { root, store, manager } = await createManager();
await manager.start("node -e \"console.log('hello from stdout'); setInterval(() => {}, 1000)\"", root);
await manager.start("node -e \"console.log('hello from stdout');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
await waitFor(() => store.getState().logHistory.some((line) => line.includes("hello from stdout")));
@@ -90,8 +90,8 @@ describe("DevServerProcessManager", () => {
it("start() throws if already running", async () => {
const { root, manager } = await createManager();
await manager.start("node -e \"setInterval(() => {}, 1000)\"", root);
await expect(manager.start("node -e \"setInterval(() => {}, 1000)\"", root)).rejects.toThrow("already running");
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
await expect(manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root)).rejects.toThrow("already running");
});
it("start() throws if command is empty", async () => {
@@ -102,7 +102,7 @@ describe("DevServerProcessManager", () => {
it("stop() sends SIGTERM and waits for exit", async () => {
const { root, store, manager } = await createManager();
await manager.start("node -e \"setInterval(() => {}, 1000)\"", root);
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
const state = await manager.stop();
expect(state.status).toBe("stopped");
@@ -119,7 +119,7 @@ describe("DevServerProcessManager", () => {
const childPidFile = join(root, "managed-child.pid");
await manager.start(
`node -e "require('node:fs').writeFileSync('${childPidFile}', String(process.pid)); setInterval(() => {}, 1000)"`,
`node -e "require('node:fs').writeFileSync('${childPidFile}', String(process.pid));process.stdin.resume();process.stdin.on('end',()=>process.exit(0))"`,
root,
);
@@ -143,7 +143,7 @@ describe("DevServerProcessManager", () => {
const { root, store, manager } = await createManager({ stopTimeoutMs: 150 });
await manager.start(
"node -e \"process.on('SIGTERM', () => {}); setInterval(() => {}, 1000)\"",
"node -e \"process.on('SIGTERM', () => {});process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
@@ -162,7 +162,7 @@ describe("DevServerProcessManager", () => {
it("restart() stops then starts with same command", async () => {
const { root, store, manager } = await createManager();
await manager.start("node -e \"setInterval(() => {}, 1000)\"", root, { scriptId: "dev" });
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root, { scriptId: "dev" });
const firstPid = store.getState().pid;
const state = await manager.restart();
@@ -176,7 +176,7 @@ describe("DevServerProcessManager", () => {
const { root, store, manager } = await createManager();
await manager.start(
"node -e \"console.log('Server ready at http://localhost:3000'); setInterval(() => {}, 1000)\"",
"node -e \"console.log('Server ready at http://localhost:3000');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
@@ -188,7 +188,7 @@ describe("DevServerProcessManager", () => {
const { root, store, manager } = await createManager();
await manager.start(
"node -e \"console.log('ready at http://127.0.0.1:4173'); setInterval(() => {}, 1000)\"",
"node -e \"console.log('ready at http://127.0.0.1:4173');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
@@ -200,7 +200,7 @@ describe("DevServerProcessManager", () => {
const { root, store, manager } = await createManager();
await manager.start(
"node -e \"console.log('Listening on port 5173'); setInterval(() => {}, 1000)\"",
"node -e \"console.log('Listening on port 5173');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
@@ -211,7 +211,7 @@ describe("DevServerProcessManager", () => {
it("schedules fallback probing after startup when no URL is announced", async () => {
const { root, manager } = await createManager({ probeDelayMs: 25, probeTimeoutMs: 5 });
await manager.start("node -e \"setInterval(() => {}, 1000)\"", root);
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
expect(manager.hasPendingProbeTimer()).toBe(true);
await waitFor(() => manager.hasPendingProbeTimer() === false, 3_000);
@@ -221,7 +221,7 @@ describe("DevServerProcessManager", () => {
const { root, store, manager } = await createManager({ probeDelayMs: 2_000, probeTimeoutMs: 5 });
await manager.start(
"node -e \"console.log('ready at http://localhost:4321'); setInterval(() => {}, 1000)\"",
"node -e \"console.log('ready at http://localhost:4321');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
@@ -232,7 +232,7 @@ describe("DevServerProcessManager", () => {
it("clears fallback probe timer on stop", async () => {
const { root, manager } = await createManager({ probeDelayMs: 2_000, probeTimeoutMs: 5 });
await manager.start("node -e \"setInterval(() => {}, 1000)\"", root);
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
expect(manager.hasPendingProbeTimer()).toBe(true);
await manager.stop();
@@ -254,7 +254,7 @@ describe("DevServerProcessManager", () => {
it("restarts with a fresh fallback probe timer", async () => {
const { root, manager } = await createManager({ probeDelayMs: 2_000, probeTimeoutMs: 5 });
await manager.start("node -e \"setInterval(() => {}, 1000)\"", root, { scriptId: "dev" });
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root, { scriptId: "dev" });
expect(manager.hasPendingProbeTimer()).toBe(true);
await manager.restart();
@@ -265,7 +265,7 @@ describe("DevServerProcessManager", () => {
it("cleanup() kills process and clears listeners", async () => {
const { root, manager } = await createManager();
await manager.start("node -e \"setInterval(() => {}, 1000)\"", root);
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
manager.on("output", () => undefined);
expect(manager.listenerCount("output")).toBeGreaterThan(0);

View File

@@ -222,7 +222,7 @@ describe("createDevServerRouter", () => {
app,
"POST",
"/api/dev-server/start",
JSON.stringify({ command: "node -e \"setInterval(() => {}, 1000)\"" }),
JSON.stringify({ command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"" }),
{ "Content-Type": "application/json" },
);
@@ -238,7 +238,7 @@ describe("createDevServerRouter", () => {
app,
"POST",
"/api/dev-server/start",
JSON.stringify({ command: "node -e \"setInterval(() => {}, 1000)\"", cwd: root }),
JSON.stringify({ command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", cwd: root }),
{ "Content-Type": "application/json" },
);
@@ -258,7 +258,7 @@ describe("createDevServerRouter", () => {
app,
"POST",
"/api/dev-server/start",
JSON.stringify({ command: "node -e \"setInterval(() => {}, 1000)\"", cwd: root }),
JSON.stringify({ command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", cwd: root }),
{ "Content-Type": "application/json" },
);
@@ -266,7 +266,7 @@ describe("createDevServerRouter", () => {
app,
"POST",
"/api/dev-server/start",
JSON.stringify({ command: "node -e \"setInterval(() => {}, 1000)\"", cwd: root }),
JSON.stringify({ command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", cwd: root }),
{ "Content-Type": "application/json" },
);
@@ -282,7 +282,7 @@ describe("createDevServerRouter", () => {
app,
"POST",
"/api/dev-server/start",
JSON.stringify({ command: "node -e \"setInterval(() => {}, 1000)\"", cwd: root }),
JSON.stringify({ command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", cwd: root }),
{ "Content-Type": "application/json" },
);
@@ -311,7 +311,7 @@ describe("createDevServerRouter", () => {
app,
"POST",
"/api/dev-server/start",
JSON.stringify({ command: "node -e \"setInterval(() => {}, 1000)\"", cwd: root, scriptId: "dev" }),
JSON.stringify({ command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", cwd: root, scriptId: "dev" }),
{ "Content-Type": "application/json" },
);
@@ -319,7 +319,7 @@ describe("createDevServerRouter", () => {
expect(restartRes.status).toBe(200);
expect(restartRes.body).toMatchObject({
status: "running",
command: "node -e \"setInterval(() => {}, 1000)\"",
command: "node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
scriptId: "dev",
});
});