import { describe, expect, it, vi } from "vitest"; const acquireGatewayLock = vi.fn(async () => ({ release: vi.fn(async () => {}), })); const consumeGatewaySigusr1RestartAuthorization = vi.fn(() => true); const isGatewaySigusr1RestartExternallyAllowed = vi.fn(() => false); const markGatewaySigusr1RestartHandled = vi.fn(); const getActiveTaskCount = vi.fn(() => 0); const waitForActiveTasks = vi.fn(async () => ({ drained: true })); const resetAllLanes = vi.fn(); const DRAIN_TIMEOUT_LOG = "drain timeout reached; proceeding with restart"; const gatewayLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; vi.mock("../../infra/gateway-lock.js", () => ({ acquireGatewayLock: () => acquireGatewayLock(), })); vi.mock("../../infra/restart.js", () => ({ consumeGatewaySigusr1RestartAuthorization: () => consumeGatewaySigusr1RestartAuthorization(), isGatewaySigusr1RestartExternallyAllowed: () => isGatewaySigusr1RestartExternallyAllowed(), markGatewaySigusr1RestartHandled: () => markGatewaySigusr1RestartHandled(), })); vi.mock("../../process/command-queue.js", () => ({ getActiveTaskCount: () => getActiveTaskCount(), waitForActiveTasks: (timeoutMs: number) => waitForActiveTasks(timeoutMs), resetAllLanes: () => resetAllLanes(), })); vi.mock("../../logging/subsystem.js", () => ({ createSubsystemLogger: () => gatewayLog, })); function removeNewSignalListeners( signal: NodeJS.Signals, existing: Set<(...args: unknown[]) => void>, ) { for (const listener of process.listeners(signal)) { const fn = listener as (...args: unknown[]) => void; if (!existing.has(fn)) { process.removeListener(signal, fn); } } } describe("runGatewayLoop", () => { it("restarts after SIGUSR1 even when drain times out, and resets lanes for the new iteration", async () => { vi.clearAllMocks(); getActiveTaskCount.mockReturnValueOnce(2).mockReturnValueOnce(0); waitForActiveTasks.mockResolvedValueOnce({ drained: false }); type StartServer = () => Promise<{ close: (opts: { reason: string; restartExpectedMs: number | null }) => Promise; }>; const closeFirst = vi.fn(async () => {}); const closeSecond = vi.fn(async () => {}); const start = vi .fn() .mockResolvedValueOnce({ close: closeFirst }) .mockResolvedValueOnce({ close: closeSecond }) .mockRejectedValueOnce(new Error("stop-loop")); const beforeSigterm = new Set( process.listeners("SIGTERM") as Array<(...args: unknown[]) => void>, ); const beforeSigint = new Set( process.listeners("SIGINT") as Array<(...args: unknown[]) => void>, ); const beforeSigusr1 = new Set( process.listeners("SIGUSR1") as Array<(...args: unknown[]) => void>, ); const loopPromise = import("./run-loop.js").then(({ runGatewayLoop }) => runGatewayLoop({ start, runtime: { exit: vi.fn(), } as { exit: (code: number) => never }, }), ); try { await vi.waitFor(() => { expect(start).toHaveBeenCalledTimes(1); }); process.emit("SIGUSR1"); await vi.waitFor(() => { expect(start).toHaveBeenCalledTimes(2); }); expect(waitForActiveTasks).toHaveBeenCalledWith(30_000); expect(gatewayLog.warn).toHaveBeenCalledWith(DRAIN_TIMEOUT_LOG); expect(closeFirst).toHaveBeenCalledWith({ reason: "gateway restarting", restartExpectedMs: 1500, }); expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(1); expect(resetAllLanes).toHaveBeenCalledTimes(1); process.emit("SIGUSR1"); await expect(loopPromise).rejects.toThrow("stop-loop"); expect(closeSecond).toHaveBeenCalledWith({ reason: "gateway restarting", restartExpectedMs: 1500, }); expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2); expect(resetAllLanes).toHaveBeenCalledTimes(2); } finally { removeNewSignalListeners("SIGTERM", beforeSigterm); removeNewSignalListeners("SIGINT", beforeSigint); removeNewSignalListeners("SIGUSR1", beforeSigusr1); } }); });