import { describe, expect, it, vi } from "vitest"; import { createLogger, errorContext, parseLogLevel } from "@/worker/logger"; const firstJsonCall = (spy: { mock: { calls: Array> } }): Record => { const payload = spy.mock.calls[0]?.[0]; if (typeof payload !== "string") { throw new Error("expected logger to write a JSON string"); } return JSON.parse(payload) as Record; }; describe("structured worker logger", () => { it("emits structured JSON and removes undefined fields", () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); const logger = createLogger("info", { requestId: "req_123", skippedBase: undefined, component: "test.component" }); logger.info("thing_happened", { count: 2, skippedField: undefined }); expect(logSpy).toHaveBeenCalledTimes(1); const entry = firstJsonCall(logSpy); expect(entry).toMatchObject({ level: "info", component: "test.component", event: "thing_happened", requestId: "req_123", count: 2, }); expect(typeof entry.ts).toBe("string"); expect(entry).not.toHaveProperty("skippedBase"); expect(entry).not.toHaveProperty("skippedField"); logSpy.mockRestore(); }); it("filters debug logs unless the level is debug; two loggers at different levels do not race", () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); const infoLogger = createLogger("info", { component: "info.scope" }); const debugLogger = createLogger("debug", { component: "debug.scope" }); infoLogger.debug("hidden"); expect(logSpy).not.toHaveBeenCalled(); debugLogger.debug("visible"); expect(logSpy).toHaveBeenCalledTimes(1); expect(firstJsonCall(logSpy)).toMatchObject({ level: "debug", event: "visible", component: "debug.scope" }); logSpy.mockClear(); // The info logger keeps filtering even after a higher-verbosity peer // logged. Without per-logger captured levels, a module-global mutable // would race here. infoLogger.debug("still hidden"); expect(logSpy).not.toHaveBeenCalled(); logSpy.mockRestore(); }); it("routes warn and error levels to matching console methods", () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); const logger = createLogger("info", { component: "test.component" }); logger.warn("warning"); logger.error("failure"); expect(logSpy).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledTimes(1); expect(errorSpy).toHaveBeenCalledTimes(1); expect(firstJsonCall(warnSpy)).toMatchObject({ level: "warn", event: "warning" }); expect(firstJsonCall(errorSpy)).toMatchObject({ level: "error", event: "failure" }); logSpy.mockRestore(); warnSpy.mockRestore(); errorSpy.mockRestore(); }); it("merges child logger fields into entries", () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); const logger = createLogger("info", { component: "test.component", requestId: "req_123" }).child({ userId: "user_123", }); logger.info("child_event"); expect(firstJsonCall(logSpy)).toMatchObject({ requestId: "req_123", userId: "user_123", }); logSpy.mockRestore(); }); it("formats error context and gates errorStack on the caller's level", () => { const error = new Error("boom"); expect(errorContext(error, "info")).toMatchObject({ errorName: "Error", errorMessage: "boom", }); expect(errorContext(error, "info")).not.toHaveProperty("errorStack"); expect(typeof errorContext(error, "debug").errorStack).toBe("string"); expect(errorContext("plain failure", "info")).toEqual({ errorMessage: "plain failure" }); }); it("parseLogLevel normalises unknown values to info", () => { expect(parseLogLevel("DEBUG")).toBe("debug"); expect(parseLogLevel("Warn")).toBe("warn"); expect(parseLogLevel(undefined)).toBe("info"); expect(parseLogLevel("trace")).toBe("info"); }); });