fix(test): detect signaled memory fd gateway exits

This commit is contained in:
Vincent Koc
2026-05-28 15:17:23 +02:00
parent 97ed582f1c
commit 5809bdf0cb
2 changed files with 71 additions and 16 deletions

View File

@@ -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 {}
}
}

View File

@@ -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");