import { spawn } from "node:child_process"; 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 = 2000): 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); }); } 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 ready = await waitForLine(child.stdout); expect(ready).toBe("ready"); // Simulate systemd sending SIGTERM to the parent process. if (!addedSigterm) { throw new Error("expected SIGTERM listener"); } addedSigterm("SIGTERM"); await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("timeout waiting for child exit")), 2_000); child.once("exit", () => { clearTimeout(timeout); resolve(); }); }); }, 5_000); });