import { expect, test, describe, beforeAll, afterAll, spyOn } from "bun:test"; import { logger } from "./logger"; import { existsSync, unlinkSync, readFileSync, writeFileSync } from "fs"; import { join } from "path"; describe("Logger", () => { const logDir = join(process.cwd(), "logs"); const logFile = join(logDir, "error.log"); beforeAll(() => { // Cleanup if exists try { if (existsSync(logFile)) unlinkSync(logFile); } catch (e) {} }); test("should log info messages to console with correct format", () => { const spy = spyOn(console, "log"); const message = "Formatting test"; logger.info("system", message); expect(spy).toHaveBeenCalled(); const callArgs = spy.mock.calls[0]?.[0]; expect(callArgs).toBeDefined(); if (callArgs) { // Strict regex check for ISO timestamp and format const regex = /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[INFO\] \[SYSTEM\] Formatting test$/; expect(callArgs).toMatch(regex); } spy.mockRestore(); }); test("should write error logs to file with stack trace", async () => { const errorMessage = "Test error message"; const testError = new Error("Source error"); logger.error("system", errorMessage, testError); // Polling for file write instead of fixed timeout let content = ""; for (let i = 0; i < 20; i++) { if (existsSync(logFile)) { content = readFileSync(logFile, "utf-8"); if (content.includes("Source error")) break; } await new Promise(resolve => setTimeout(resolve, 50)); } expect(content).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[ERROR\] \[SYSTEM\] Test error message: Source error/); expect(content).toContain("Stack Trace:"); expect(content).toContain("Error: Source error"); expect(content).toContain("logger.test.ts"); }); test("should handle log directory creation failures gracefully", async () => { const consoleSpy = spyOn(console, "error"); // We trigger an error by trying to use a path that is a file where a directory should be const triggerFile = join(process.cwd(), "logs_fail_trigger"); try { writeFileSync(triggerFile, "not a directory"); // Manually override paths for this test instance const originalLogDir = (logger as any).logDir; const originalLogPath = (logger as any).errorLogPath; (logger as any).logDir = triggerFile; (logger as any).errorLogPath = join(triggerFile, "error.log"); (logger as any).initialized = false; logger.error("system", "This should fail directory creation"); // Wait for async initialization attempt await new Promise(resolve => setTimeout(resolve, 100)); expect(consoleSpy).toHaveBeenCalled(); expect(consoleSpy.mock.calls.some(call => String(call[0]).includes("Failed to initialize logger directory") )).toBe(true); // Reset logger state (logger as any).logDir = originalLogDir; (logger as any).errorLogPath = originalLogPath; (logger as any).initialized = false; } finally { if (existsSync(triggerFile)) unlinkSync(triggerFile); consoleSpy.mockRestore(); } }); test("should include complex data objects in logs", () => { const spy = spyOn(console, "log"); const data = { userId: "123", tags: ["test"] }; logger.info("bot", "Message with data", data); expect(spy).toHaveBeenCalled(); const callArgs = spy.mock.calls[0]?.[0]; expect(callArgs).toBeDefined(); if (callArgs) { expect(callArgs).toContain(` | Data: ${JSON.stringify(data)}`); } spy.mockRestore(); }); test("should handle circular references in data objects", () => { const spy = spyOn(console, "log"); const data: any = { name: "circular" }; data.self = data; logger.info("bot", "Circular test", data); expect(spy).toHaveBeenCalled(); const callArgs = spy.mock.calls[0]?.[0]; expect(callArgs).toContain("[Circular]"); spy.mockRestore(); }); });