/**
* Tests for debug utilities
*
* Comprehensive test coverage for debug logging, performance timing,
* and development utilities.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { debug, startTimer } from "@/utils/debug.js";
import { Config } from "@/config/Config.js";
describe("Debug Utilities", () => {
let originalConsole;
let _consoleLogSpy;
let _consoleWarnSpy;
let consoleErrorSpy;
let originalNodeEnv;
let originalDebug;
beforeEach(() => {
vi.clearAllMocks();
// Store original console methods
originalConsole = { ...console };
// Create console spies - debug utility uses console.error to avoid STDIO interference
_consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
_consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
// Store original environment variables
originalNodeEnv = process.env.NODE_ENV;
originalDebug = process.env.DEBUG;
// Reset Config singleton to pick up new environment variables
Config.reset();
});
afterEach(() => {
// Restore console methods
console.log = originalConsole.log;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
// Restore environment variables
process.env.NODE_ENV = originalNodeEnv;
if (originalDebug !== undefined) {
process.env.DEBUG = originalDebug;
Config.reset(); // Reload config with new env vars
} else {
delete process.env.DEBUG;
}
// Reset Config singleton after test
Config.reset();
vi.restoreAllMocks();
});
describe("Debug Logger", () => {
it("should log messages when in debug mode", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
Config.reset(); // Reload config with new env vars
debug.log("Test debug message");
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Test debug message"));
});
it("should not log messages in production", () => {
process.env.DEBUG = "false";
Config.reset(); // Reload config with new env vars
Config.reset(); // Reload config with new env vars
Config.reset(); // Reload config with new env vars
debug.log("Test debug message");
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it("should log messages when DEBUG environment variable is set", () => {
process.env.NODE_ENV = "production";
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
debug.log("Test debug message");
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Test debug message"));
delete process.env.DEBUG;
});
it("should handle object logging", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
const testObject = {
id: 123,
name: "Test Object",
data: { nested: "value" },
};
debug.log("Object test", testObject);
// debug.log formats everything into a single string with timestamp
expect(consoleErrorSpy).toHaveBeenCalled();
const callArg = consoleErrorSpy.mock.calls[0][0];
expect(callArg).toContain("Object test");
expect(callArg).toContain("Test Object");
});
it("should handle array logging", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
const testArray = [1, 2, 3, "test", { key: "value" }];
debug.log("Array test", testArray);
// debug.log formats everything into a single string with timestamp
expect(consoleErrorSpy).toHaveBeenCalled();
const callArg = consoleErrorSpy.mock.calls[0][0];
expect(callArg).toContain("Array test");
});
it("should handle multiple arguments", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
debug.log("Multiple", "arguments", { test: true }, 123);
// debug.log formats everything into a single string with timestamp
expect(consoleErrorSpy).toHaveBeenCalled();
const callArg = consoleErrorSpy.mock.calls[0][0];
expect(callArg).toContain("Multiple");
expect(callArg).toContain("arguments");
expect(callArg).toContain("123");
});
it("should handle null and undefined values", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
debug.log("Null test", null);
debug.log("Undefined test", undefined);
// debug.log formats everything into a single string with timestamp
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
expect(consoleErrorSpy.mock.calls[0][0]).toContain("Null test");
expect(consoleErrorSpy.mock.calls[1][0]).toContain("Undefined test");
});
it("should add timestamp to log messages", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
debug.log("Timestamp test");
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/),
);
delete process.env.DEBUG;
});
it("should handle empty messages", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
debug.log("");
debug.log();
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
delete process.env.DEBUG;
});
it("should handle error objects", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
const error = new Error("Test error");
error.code = "TEST_ERROR";
error.statusCode = 500;
debug.log("Error test", error);
// debug.log formats everything into a single string with timestamp
expect(consoleErrorSpy).toHaveBeenCalled();
const callArg = consoleErrorSpy.mock.calls[0][0];
expect(callArg).toContain("Error test");
});
it("should handle circular references safely", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
const circular = { name: "circular" };
circular.self = circular;
// Currently formatMessage uses JSON.stringify which throws on circular refs
// This is a known limitation - formatMessage should use safeStringify instead
expect(() => {
debug.log("Circular test", circular);
}).toThrow(/circular structure/i);
});
});
describe("Performance Timer", () => {
it("should create a timer object", () => {
const timer = startTimer();
expect(timer).toHaveProperty("end");
expect(typeof timer.end).toBe("function");
});
it("should measure elapsed time", async () => {
const timer = startTimer();
// Wait for a small delay
await new Promise((resolve) => setTimeout(resolve, 10));
const elapsed = timer.end();
expect(elapsed).toBeGreaterThan(5);
expect(elapsed).toBeLessThan(50); // Should be around 10ms
});
it("should return time in milliseconds", () => {
const timer = startTimer();
const elapsed = timer.end();
expect(typeof elapsed).toBe("number");
expect(elapsed).toBeGreaterThanOrEqual(0);
});
it("should handle multiple calls to end()", () => {
const timer = startTimer();
const elapsed1 = timer.end();
const elapsed2 = timer.end();
expect(elapsed1).toBeGreaterThanOrEqual(0);
expect(elapsed2).toBeGreaterThanOrEqual(elapsed1);
});
it("should create independent timers", async () => {
const timer1 = startTimer();
await new Promise((resolve) => setTimeout(resolve, 5));
const timer2 = startTimer();
await new Promise((resolve) => setTimeout(resolve, 10));
const elapsed1 = timer1.end();
const elapsed2 = timer2.end();
expect(elapsed1).toBeGreaterThan(elapsed2);
});
it("should handle high precision timing", () => {
const timer = startTimer();
// Do some synchronous work
let _sum = 0;
for (let i = 0; i < 1000; i++) {
_sum += i;
}
const elapsed = timer.end();
expect(elapsed).toBeGreaterThanOrEqual(0);
expect(typeof elapsed).toBe("number");
expect(Number.isFinite(elapsed)).toBe(true);
});
it("should work with async operations", async () => {
const timer = startTimer();
await Promise.resolve("test");
await new Promise((resolve) => setTimeout(resolve, 1));
const elapsed = timer.end();
expect(elapsed).toBeGreaterThan(0);
});
});
describe("Debug Conditional Logging", () => {
it("should respect NODE_ENV=test", () => {
process.env.NODE_ENV = "test";
debug.log("Test environment message");
// In test environment, debug logging might be disabled
// This tests the conditional logic
expect(consoleErrorSpy.mock.calls.length).toBeGreaterThanOrEqual(0);
});
it("should respect DEBUG=false explicitly", () => {
process.env.DEBUG = "false";
Config.reset(); // Reload config with new env vars
Config.reset(); // Reload config with new env vars
debug.log("Debug disabled message");
// Should not log when explicitly disabled
expect(consoleErrorSpy).not.toHaveBeenCalled();
delete process.env.DEBUG;
});
it("should handle various truthy DEBUG values", () => {
// Only "true" enables debug in the config
consoleErrorSpy.mockClear();
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
debug.log("Debug with true");
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Debug with true"));
delete process.env.DEBUG;
});
it("should handle various falsy DEBUG values", () => {
const falsyValues = ["false", "0", "no", "off", undefined];
falsyValues.forEach((value) => {
consoleErrorSpy.mockClear();
if (value === undefined) {
delete process.env.DEBUG;
} else {
process.env.DEBUG = value;
}
Config.reset(); // Reload config with new env vars after each change
debug.log(`Debug with ${value}`);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
});
describe("Error Handling in Debug", () => {
it("should handle logging errors gracefully", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
// Mock console.error to throw an error
consoleErrorSpy.mockImplementation(() => {
throw new Error("Console error");
});
expect(() => {
debug.log("This should not crash");
}).toThrow(); // Will throw since console.error throws
delete process.env.DEBUG;
});
it("should handle JSON serialization errors", () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
const problematicObject = {};
// Create a circular reference that would break JSON.stringify
problematicObject.circular = problematicObject;
// Currently formatMessage uses JSON.stringify which throws on circular refs
// This is a known limitation - formatMessage should use safeStringify instead
expect(() => {
debug.log("Problematic object", problematicObject);
}).toThrow(/circular structure/i);
});
});
describe("Performance in Production", () => {
it("should have minimal overhead when disabled", () => {
process.env.NODE_ENV = "production";
delete process.env.DEBUG;
const timer = startTimer();
// Make many debug calls - should be fast since they're disabled
for (let i = 0; i < 1000; i++) {
debug.log("Disabled message", i, { data: "test" });
}
const elapsed = timer.end();
// Should be very fast since debug is disabled
expect(elapsed).toBeLessThan(100);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it("should create minimal timer overhead", () => {
const iterations = 1000;
const timers = [];
const startTime = Date.now();
for (let i = 0; i < iterations; i++) {
const timer = startTimer();
timers.push(timer.end());
}
const totalTime = Date.now() - startTime;
expect(totalTime).toBeLessThan(100); // Should be very fast
expect(timers).toHaveLength(iterations);
expect(timers.every((time) => typeof time === "number")).toBe(true);
});
});
describe("Integration Patterns", () => {
it("should work with async/await patterns", async () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
const timer = startTimer();
const result = await (async () => {
debug.log("Starting async operation");
await new Promise((resolve) => setTimeout(resolve, 5));
debug.log("Async operation complete");
return "success";
})();
const elapsed = timer.end();
expect(result).toBe("success");
expect(elapsed).toBeGreaterThanOrEqual(4);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Starting async operation"));
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Async operation complete"));
delete process.env.DEBUG;
});
it("should work with Promise chains", async () => {
process.env.DEBUG = "true";
Config.reset(); // Reload config with new env vars
const result = await Promise.resolve("initial")
.then((value) => {
debug.log("First then", value);
return `${value}-processed`;
})
.then((value) => {
debug.log("Second then", value);
return `${value}-final`;
})
.catch((error) => {
debug.log("Promise error", error);
throw error;
});
expect(result).toBe("initial-processed-final");
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
expect(consoleErrorSpy.mock.calls[0][0]).toContain("First then");
expect(consoleErrorSpy.mock.calls[0][0]).toContain("initial");
expect(consoleErrorSpy.mock.calls[1][0]).toContain("Second then");
expect(consoleErrorSpy.mock.calls[1][0]).toContain("initial-processed");
});
it("should handle nested timer scenarios", async () => {
const outerTimer = startTimer();
await new Promise((resolve) => setTimeout(resolve, 5));
const innerTimer = startTimer();
await new Promise((resolve) => setTimeout(resolve, 10));
const innerElapsed = innerTimer.end();
const outerElapsed = outerTimer.end();
expect(innerElapsed).toBeGreaterThan(8);
expect(outerElapsed).toBeGreaterThan(innerElapsed);
expect(outerElapsed).toBeGreaterThan(13);
});
});
});