import { afterEach, expect, test, vi } from "vitest" import { observeNodejsExceptions, onNodejsUncaughtException, onNodejsUncaughtExceptionMonitor, onNodejsUnhandledRejection, } from "#Source/exception/index.ts" class ProcessMock { protected listeners = new Map void>>() versions = { node: "20.0.0" } on(eventName: string, listener: (...args: unknown[]) => void): this { const eventListeners = this.listeners.get(eventName) ?? new Set() eventListeners.add(listener) this.listeners.set(eventName, eventListeners) return this } off(eventName: string, listener: (...args: unknown[]) => void): this { this.listeners.get(eventName)?.delete(listener) return this } emit(eventName: string, ...args: unknown[]): void { for (const listener of this.listeners.get(eventName) ?? []) { listener(...args) } } } afterEach(() => { vi.unstubAllGlobals() vi.restoreAllMocks() }) test("onNodejsUncaughtExceptionMonitor listens and cleans up monitor events", () => { const mockedProcess = new ProcessMock() const listener = vi.fn() vi.stubGlobal("process", mockedProcess) const cleanup = onNodejsUncaughtExceptionMonitor(listener) mockedProcess.emit("uncaughtExceptionMonitor", new Error("boom"), "uncaughtException") expect(listener).toHaveBeenCalledWith( expect.objectContaining({ runtime: "nodejs", source: "nodejs.uncaught-exception-monitor", message: "boom", origin: "uncaughtException", }), ) cleanup() mockedProcess.emit("uncaughtExceptionMonitor", new Error("boom"), "uncaughtException") expect(listener).toHaveBeenCalledTimes(1) }) test("onNodejsUncaughtException normalizes records and cleans up listeners", () => { const mockedProcess = new ProcessMock() const listener = vi.fn() vi.stubGlobal("process", mockedProcess) const cleanup = onNodejsUncaughtException(listener) mockedProcess.emit("uncaughtException", new Error("broken"), "uncaughtException") expect(listener).toHaveBeenCalledWith( expect.objectContaining({ source: "nodejs.uncaught-exception", message: "broken", origin: "uncaughtException", }), ) cleanup() mockedProcess.emit("uncaughtException", new Error("ignored"), "uncaughtException") expect(listener).toHaveBeenCalledTimes(1) }) test("onNodejsUnhandledRejection normalizes records and cleans up listeners", () => { const mockedProcess = new ProcessMock() const listener = vi.fn() vi.stubGlobal("process", mockedProcess) const cleanup = onNodejsUnhandledRejection(listener) const promise = Promise.resolve("ok") mockedProcess.emit("unhandledRejection", "reason-text", promise) expect(listener).toHaveBeenCalledWith( expect.objectContaining({ source: "nodejs.unhandled-rejection", message: "reason-text", promise, }), ) cleanup() mockedProcess.emit("unhandledRejection", "ignored", promise) expect(listener).toHaveBeenCalledTimes(1) }) test("observeNodejsExceptions listens to all Node.js sources by default and supports explicit opt-out", () => { const mockedProcess = new ProcessMock() const listener = vi.fn() vi.stubGlobal("process", mockedProcess) const cleanup1 = observeNodejsExceptions(listener, { captureTimestamp: () => 456, }) const promise = Promise.resolve("ok") mockedProcess.emit("uncaughtExceptionMonitor", new Error("watch"), "uncaughtException") mockedProcess.emit("uncaughtException", new Error("boom"), "uncaughtException") mockedProcess.emit("unhandledRejection", new Error("reject"), promise) expect(listener).toHaveBeenCalledTimes(3) expect(listener).toHaveBeenNthCalledWith( 1, expect.objectContaining({ source: "nodejs.uncaught-exception-monitor", timestamp: 456, }), ) expect(listener).toHaveBeenNthCalledWith( 2, expect.objectContaining({ source: "nodejs.uncaught-exception", timestamp: 456, }), ) expect(listener).toHaveBeenNthCalledWith( 3, expect.objectContaining({ source: "nodejs.unhandled-rejection", timestamp: 456, }), ) cleanup1() listener.mockClear() const cleanup2 = observeNodejsExceptions(listener, { includeUncaughtExceptionMonitor: false, }) mockedProcess.emit("uncaughtExceptionMonitor", new Error("ignored"), "uncaughtException") mockedProcess.emit("uncaughtException", new Error("kept"), "uncaughtException") mockedProcess.emit("unhandledRejection", "kept", promise) expect(listener).toHaveBeenCalledTimes(2) expect(listener).not.toHaveBeenCalledWith( expect.objectContaining({ source: "nodejs.uncaught-exception-monitor", }), ) cleanup2() })