import os from "node:os"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { __testing, consumeGatewaySigusr1RestartAuthorization, emitGatewayRestart, isGatewaySigusr1RestartExternallyAllowed, markGatewaySigusr1RestartHandled, scheduleGatewaySigusr1Restart, setGatewaySigusr1RestartPolicy, setPreRestartDeferralCheck, } from "./restart.js"; import { listTailnetAddresses } from "./tailnet.js"; describe("infra runtime", () => { function setupRestartSignalSuite() { beforeEach(() => { __testing.resetSigusr1State(); vi.useFakeTimers(); vi.spyOn(process, "kill").mockImplementation(() => true); }); afterEach(async () => { await vi.runOnlyPendingTimersAsync(); vi.useRealTimers(); vi.restoreAllMocks(); __testing.resetSigusr1State(); }); } describe("restart authorization", () => { setupRestartSignalSuite(); it("authorizes exactly once when scheduled restart emits", async () => { expect(consumeGatewaySigusr1RestartAuthorization()).toBe(false); scheduleGatewaySigusr1Restart({ delayMs: 0 }); // No pre-authorization before the scheduled emission fires. expect(consumeGatewaySigusr1RestartAuthorization()).toBe(false); await vi.advanceTimersByTimeAsync(0); expect(consumeGatewaySigusr1RestartAuthorization()).toBe(true); expect(consumeGatewaySigusr1RestartAuthorization()).toBe(false); await vi.runAllTimersAsync(); }); it("tracks external restart policy", () => { expect(isGatewaySigusr1RestartExternallyAllowed()).toBe(false); setGatewaySigusr1RestartPolicy({ allowExternal: true }); expect(isGatewaySigusr1RestartExternallyAllowed()).toBe(true); }); it("suppresses duplicate emit until the restart cycle is marked handled", () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; process.on("SIGUSR1", handler); try { expect(emitGatewayRestart()).toBe(true); expect(emitGatewayRestart()).toBe(false); expect(consumeGatewaySigusr1RestartAuthorization()).toBe(true); markGatewaySigusr1RestartHandled(); expect(emitGatewayRestart()).toBe(true); const sigusr1Emits = emitSpy.mock.calls.filter((args) => args[0] === "SIGUSR1"); expect(sigusr1Emits.length).toBe(2); } finally { process.removeListener("SIGUSR1", handler); } }); it("coalesces duplicate scheduled restarts into a single pending timer", async () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; process.on("SIGUSR1", handler); try { const first = scheduleGatewaySigusr1Restart({ delayMs: 1_000, reason: "first" }); const second = scheduleGatewaySigusr1Restart({ delayMs: 1_000, reason: "second" }); expect(first.coalesced).toBe(false); expect(second.coalesced).toBe(true); await vi.advanceTimersByTimeAsync(999); expect(emitSpy).not.toHaveBeenCalledWith("SIGUSR1"); await vi.advanceTimersByTimeAsync(1); const sigusr1Emits = emitSpy.mock.calls.filter((args) => args[0] === "SIGUSR1"); expect(sigusr1Emits.length).toBe(1); } finally { process.removeListener("SIGUSR1", handler); } }); it("applies restart cooldown between emitted restart cycles", async () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; process.on("SIGUSR1", handler); try { const first = scheduleGatewaySigusr1Restart({ delayMs: 0, reason: "first" }); expect(first.coalesced).toBe(false); expect(first.delayMs).toBe(0); await vi.advanceTimersByTimeAsync(0); expect(consumeGatewaySigusr1RestartAuthorization()).toBe(true); markGatewaySigusr1RestartHandled(); const second = scheduleGatewaySigusr1Restart({ delayMs: 0, reason: "second" }); expect(second.coalesced).toBe(false); expect(second.delayMs).toBe(30_000); expect(second.cooldownMsApplied).toBe(30_000); await vi.advanceTimersByTimeAsync(29_999); expect(emitSpy.mock.calls.filter((args) => args[0] === "SIGUSR1").length).toBe(1); await vi.advanceTimersByTimeAsync(1); expect(emitSpy.mock.calls.filter((args) => args[0] === "SIGUSR1").length).toBe(2); } finally { process.removeListener("SIGUSR1", handler); } }); }); describe("pre-restart deferral check", () => { setupRestartSignalSuite(); it("emits SIGUSR1 immediately when no deferral check is registered", async () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; process.on("SIGUSR1", handler); try { scheduleGatewaySigusr1Restart({ delayMs: 0 }); await vi.advanceTimersByTimeAsync(0); expect(emitSpy).toHaveBeenCalledWith("SIGUSR1"); } finally { process.removeListener("SIGUSR1", handler); } }); it("emits SIGUSR1 immediately when deferral check returns 0", async () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; process.on("SIGUSR1", handler); try { setPreRestartDeferralCheck(() => 0); scheduleGatewaySigusr1Restart({ delayMs: 0 }); await vi.advanceTimersByTimeAsync(0); expect(emitSpy).toHaveBeenCalledWith("SIGUSR1"); } finally { process.removeListener("SIGUSR1", handler); } }); it("defers SIGUSR1 until deferral check returns 0", async () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; process.on("SIGUSR1", handler); try { let pending = 2; setPreRestartDeferralCheck(() => pending); scheduleGatewaySigusr1Restart({ delayMs: 0 }); // After initial delay fires, deferral check returns 2 — should NOT emit yet await vi.advanceTimersByTimeAsync(0); expect(emitSpy).not.toHaveBeenCalledWith("SIGUSR1"); // After one poll (500ms), still pending await vi.advanceTimersByTimeAsync(500); expect(emitSpy).not.toHaveBeenCalledWith("SIGUSR1"); // Drain pending work pending = 0; await vi.advanceTimersByTimeAsync(500); expect(emitSpy).toHaveBeenCalledWith("SIGUSR1"); } finally { process.removeListener("SIGUSR1", handler); } }); it("emits SIGUSR1 after deferral timeout even if still pending", async () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; process.on("SIGUSR1", handler); try { setPreRestartDeferralCheck(() => 5); // always pending scheduleGatewaySigusr1Restart({ delayMs: 0 }); // Fire initial timeout await vi.advanceTimersByTimeAsync(0); expect(emitSpy).not.toHaveBeenCalledWith("SIGUSR1"); // Advance past the 90s max deferral wait await vi.advanceTimersByTimeAsync(90_000); expect(emitSpy).toHaveBeenCalledWith("SIGUSR1"); } finally { process.removeListener("SIGUSR1", handler); } }); it("emits SIGUSR1 if deferral check throws", async () => { const emitSpy = vi.spyOn(process, "emit"); const handler = () => {}; process.on("SIGUSR1", handler); try { setPreRestartDeferralCheck(() => { throw new Error("boom"); }); scheduleGatewaySigusr1Restart({ delayMs: 0 }); await vi.advanceTimersByTimeAsync(0); expect(emitSpy).toHaveBeenCalledWith("SIGUSR1"); } finally { process.removeListener("SIGUSR1", handler); } }); }); describe("tailnet address detection", () => { it("detects tailscale IPv4 and IPv6 addresses", () => { vi.spyOn(os, "networkInterfaces").mockReturnValue({ lo0: [{ address: "127.0.0.1", family: "IPv4", internal: true, netmask: "" }], utun9: [ { address: "100.123.224.76", family: "IPv4", internal: false, netmask: "", }, { address: "fd7a:115c:a1e0::8801:e04c", family: "IPv6", internal: false, netmask: "", }, ], // oxlint-disable-next-line typescript/no-explicit-any } as any); const out = listTailnetAddresses(); expect(out.ipv4).toEqual(["100.123.224.76"]); expect(out.ipv6).toEqual(["fd7a:115c:a1e0::8801:e04c"]); }); }); });