import { spawn } from "node:child_process"; import net from "node:net"; import path from "node:path"; import process from "node:process"; import { afterEach, describe, expect, it } from "vitest"; import { attachChildProcessBridge } from "./child-process-bridge.js"; function waitForLine(stream: NodeJS.ReadableStream, timeoutMs = 10_000): Promise { return new Promise((resolve, reject) => { let buffer = ""; const timeout = setTimeout(() => { cleanup(); reject(new Error("timeout waiting for line")); }, timeoutMs); const onData = (chunk: Buffer | string): void => { buffer += chunk.toString(); const idx = buffer.indexOf("\n"); if (idx >= 0) { const line = buffer.slice(0, idx).trim(); cleanup(); resolve(line); } }; const onError = (err: unknown): void => { cleanup(); reject(err); }; const cleanup = (): void => { clearTimeout(timeout); stream.off("data", onData); stream.off("error", onError); }; stream.on("data", onData); stream.on("error", onError); }); } function canConnect(port: number): Promise { return new Promise((resolve) => { const socket = net.createConnection({ host: "127.0.0.1", port }); socket.once("connect", () => { socket.end(); resolve(true); }); socket.once("error", () => resolve(false)); }); } describe("attachChildProcessBridge", () => { const children: Array<{ kill: (signal?: NodeJS.Signals) => boolean }> = []; const detachments: Array<() => void> = []; afterEach(() => { for (const detach of detachments) { try { detach(); } catch { // ignore } } detachments.length = 0; for (const child of children) { try { child.kill("SIGKILL"); } catch { // ignore } } children.length = 0; }); it("forwards SIGTERM to the wrapped child", async () => { const childPath = path.resolve(process.cwd(), "test/fixtures/child-process-bridge/child.js"); const beforeSigterm = new Set(process.listeners("SIGTERM")); const child = spawn(process.execPath, [childPath], { stdio: ["ignore", "pipe", "inherit"], env: process.env, }); const { detach } = attachChildProcessBridge(child); detachments.push(detach); children.push(child); const afterSigterm = process.listeners("SIGTERM"); const addedSigterm = afterSigterm.find((listener) => !beforeSigterm.has(listener)); if (!child.stdout) throw new Error("expected stdout"); const portLine = await waitForLine(child.stdout); const port = Number(portLine); expect(Number.isFinite(port)).toBe(true); expect(await canConnect(port)).toBe(true); // Simulate systemd sending SIGTERM to the parent process. if (!addedSigterm) throw new Error("expected SIGTERM listener"); addedSigterm(); await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("timeout waiting for child exit")), 10_000); child.once("exit", () => { clearTimeout(timeout); resolve(); }); }); await new Promise((r) => setTimeout(r, 250)); expect(await canConnect(port)).toBe(false); }, 20_000); });