import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from "vitest"; import process from "node:process"; import { installUnhandledRejectionHandler } from "./unhandled-rejections.js"; describe("installUnhandledRejectionHandler - fatal detection", () => { let exitCalls: Array = []; let consoleErrorSpy: ReturnType; let consoleWarnSpy: ReturnType; let originalExit: typeof process.exit; beforeAll(() => { originalExit = process.exit.bind(process); installUnhandledRejectionHandler(); }); beforeEach(() => { exitCalls = []; vi.spyOn(process, "exit").mockImplementation((code: string | number | null | undefined) => { if (code !== undefined && code !== null) { exitCalls.push(code); } }); consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); }); afterEach(() => { vi.clearAllMocks(); consoleErrorSpy.mockRestore(); consoleWarnSpy.mockRestore(); }); afterAll(() => { process.exit = originalExit; }); describe("fatal errors", () => { it("exits on ERR_OUT_OF_MEMORY", () => { const oomErr = Object.assign(new Error("Out of memory"), { code: "ERR_OUT_OF_MEMORY", }); process.emit("unhandledRejection", oomErr, Promise.resolve()); expect(exitCalls).toEqual([1]); expect(consoleErrorSpy).toHaveBeenCalledWith( "[openclaw] FATAL unhandled rejection:", expect.stringContaining("Out of memory"), ); }); it("exits on ERR_SCRIPT_EXECUTION_TIMEOUT", () => { const timeoutErr = Object.assign(new Error("Script execution timeout"), { code: "ERR_SCRIPT_EXECUTION_TIMEOUT", }); process.emit("unhandledRejection", timeoutErr, Promise.resolve()); expect(exitCalls).toEqual([1]); }); it("exits on ERR_WORKER_OUT_OF_MEMORY", () => { const workerOomErr = Object.assign(new Error("Worker out of memory"), { code: "ERR_WORKER_OUT_OF_MEMORY", }); process.emit("unhandledRejection", workerOomErr, Promise.resolve()); expect(exitCalls).toEqual([1]); }); }); describe("configuration errors", () => { it("exits on INVALID_CONFIG", () => { const configErr = Object.assign(new Error("Invalid config"), { code: "INVALID_CONFIG", }); process.emit("unhandledRejection", configErr, Promise.resolve()); expect(exitCalls).toEqual([1]); expect(consoleErrorSpy).toHaveBeenCalledWith( "[openclaw] CONFIGURATION ERROR - requires fix:", expect.stringContaining("Invalid config"), ); }); it("exits on MISSING_API_KEY", () => { const missingKeyErr = Object.assign(new Error("Missing API key"), { code: "MISSING_API_KEY", }); process.emit("unhandledRejection", missingKeyErr, Promise.resolve()); expect(exitCalls).toEqual([1]); }); }); describe("non-fatal errors", () => { it("does NOT exit on undici fetch failures", () => { const fetchErr = Object.assign(new TypeError("fetch failed"), { cause: { code: "UND_ERR_CONNECT_TIMEOUT", syscall: "connect" }, }); process.emit("unhandledRejection", fetchErr, Promise.resolve()); expect(exitCalls).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalledWith( "[openclaw] Non-fatal unhandled rejection (continuing):", expect.stringContaining("fetch failed"), ); }); it("does NOT exit on DNS resolution failures", () => { const dnsErr = Object.assign(new Error("DNS resolve failed"), { code: "UND_ERR_DNS_RESOLVE_FAILED", }); process.emit("unhandledRejection", dnsErr, Promise.resolve()); expect(exitCalls).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalled(); }); it("exits on generic errors without code", () => { const genericErr = new Error("Something went wrong"); process.emit("unhandledRejection", genericErr, Promise.resolve()); expect(exitCalls).toEqual([1]); expect(consoleErrorSpy).toHaveBeenCalledWith( "[openclaw] Unhandled promise rejection:", expect.stringContaining("Something went wrong"), ); }); it("does NOT exit on connection reset errors", () => { const connResetErr = Object.assign(new Error("Connection reset"), { code: "ECONNRESET", }); process.emit("unhandledRejection", connResetErr, Promise.resolve()); expect(exitCalls).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalled(); }); it("does NOT exit on timeout errors", () => { const timeoutErr = Object.assign(new Error("Timeout"), { code: "ETIMEDOUT", }); process.emit("unhandledRejection", timeoutErr, Promise.resolve()); expect(exitCalls).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalled(); }); it("does NOT exit on mDNS/Bonjour IPv4 address change errors", () => { const mdnsErr = Object.assign( new Error("Reached illegal state! IPv4 address changed from undefined to defined!"), { name: "AssertionError", code: "ERR_ASSERTION", }, ); process.emit("unhandledRejection", mdnsErr, Promise.resolve()); expect(exitCalls).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalledWith( "[openclaw] Non-fatal unhandled rejection (continuing):", expect.stringContaining("IPv4 address changed"), ); }); it("does NOT exit on mDNS MDNSServer illegal state errors", () => { const mdnsErr = new Error("MDNSServer: Reached illegal state during network update"); process.emit("unhandledRejection", mdnsErr, Promise.resolve()); expect(exitCalls).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalled(); }); }); });