diff --git a/scripts/check-memory-fd-repro.mjs b/scripts/check-memory-fd-repro.mjs index 1ded62ebfbf..63f28534010 100644 --- a/scripts/check-memory-fd-repro.mjs +++ b/scripts/check-memory-fd-repro.mjs @@ -342,7 +342,11 @@ function sampleFds({ label, pid, workspaceRealPath }) { return sample; } -async function waitForGatewayReady({ child, port, logPath, timeoutMs }) { +export function hasChildExited(child) { + return child.exitCode !== null || child.signalCode !== null; +} + +export async function waitForGatewayReady({ child, port, logPath, timeoutMs }) { const startedAt = Date.now(); let outputState = { tail: "", readySeen: false }; const append = (chunk) => { @@ -357,7 +361,7 @@ async function waitForGatewayReady({ child, port, logPath, timeoutMs }) { if (outputState.readySeen && findGatewayPid(port)) { return; } - if (child.exitCode !== null) { + if (hasChildExited(child)) { throw new Error(`gateway exited before ready; see ${logPath}`); } await sleep(100); @@ -365,26 +369,41 @@ async function waitForGatewayReady({ child, port, logPath, timeoutMs }) { throw new Error(`gateway did not become ready within ${timeoutMs}ms; see ${logPath}`); } -async function stopGateway({ child, port }) { - if (child.exitCode === null) { +export async function stopGateway({ child, port }) { + return stopGatewayWithRuntime({ + child, + port, + findGatewayPidFn: findGatewayPid, + killProcess: process.kill, + }); +} + +export async function stopGatewayWithRuntime({ + child, + port, + findGatewayPidFn, + killProcess, + listenerSettleDelayMs = 500, +}) { + if (!hasChildExited(child)) { child.kill("SIGINT"); for (let i = 0; i < 50; i += 1) { - if (child.exitCode !== null) { + if (hasChildExited(child)) { break; } await sleep(100); } } - const listenerPid = findGatewayPid(port); + const listenerPid = findGatewayPidFn(port); if (listenerPid) { try { - process.kill(listenerPid, "SIGTERM"); + killProcess(listenerPid, "SIGTERM"); } catch {} - await sleep(500); - const stillListening = findGatewayPid(port); + await sleep(listenerSettleDelayMs); + const stillListening = findGatewayPidFn(port); if (stillListening) { try { - process.kill(stillListening, "SIGKILL"); + killProcess(stillListening, "SIGKILL"); } catch {} } } diff --git a/test/scripts/check-memory-fd-repro.test.ts b/test/scripts/check-memory-fd-repro.test.ts index 94125c6576b..42da1c104f7 100644 --- a/test/scripts/check-memory-fd-repro.test.ts +++ b/test/scripts/check-memory-fd-repro.test.ts @@ -1,10 +1,50 @@ -import { describe, expect, it } from "vitest"; +import { EventEmitter } from "node:events"; +import { describe, expect, it, vi } from "vitest"; import { GATEWAY_READY_OUTPUT_MAX_CHARS, + hasChildExited, + stopGatewayWithRuntime, updateGatewayReadyOutputState, + waitForGatewayReady, } from "../../scripts/check-memory-fd-repro.mjs"; describe("check-memory-fd-repro", () => { + it("treats signaled gateway children as exited", () => { + expect(hasChildExited({ exitCode: null, signalCode: "SIGTERM" })).toBe(true); + expect(hasChildExited({ exitCode: 0, signalCode: null })).toBe(true); + expect(hasChildExited({ exitCode: null, signalCode: null })).toBe(false); + }); + + it("fails gateway readiness immediately after signal exits", async () => { + const child = { + exitCode: null, + signalCode: "SIGTERM", + stderr: new EventEmitter(), + stdout: new EventEmitter(), + }; + + await expect( + waitForGatewayReady({ child, port: 9, logPath: "gateway.log", timeoutMs: 10_000 }), + ).rejects.toThrow("gateway exited before ready"); + }); + + it("does not signal already exited children during gateway cleanup", async () => { + const child = { + exitCode: null, + kill: vi.fn(), + signalCode: "SIGTERM", + }; + const findGatewayPidFn = vi.fn(() => null); + const killProcess = vi.fn(); + + await expect( + stopGatewayWithRuntime({ child, findGatewayPidFn, killProcess, port: 9 }), + ).resolves.toBeUndefined(); + expect(child.kill).not.toHaveBeenCalled(); + expect(findGatewayPidFn).toHaveBeenCalledWith(9); + expect(killProcess).not.toHaveBeenCalled(); + }); + it("bounds gateway readiness output while keeping newest logs", () => { const first = updateGatewayReadyOutputState({ tail: "abc", readySeen: false }, "def", 8); expect(first).toEqual({ tail: "abcdef", readySeen: false }); @@ -39,11 +79,7 @@ describe("check-memory-fd-repro", () => { }); it("preserves previous readiness once seen", () => { - const state = updateGatewayReadyOutputState( - { tail: "old", readySeen: true }, - "new output", - 8, - ); + const state = updateGatewayReadyOutputState({ tail: "old", readySeen: true }, "new output", 8); expect(state.readySeen).toBe(true); expect(state.tail).toBe("w output");