logger.test.ts•6.85 kB
import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { configureLogger } from './logger.js';
import logger from './logger.js';
describe('logger configuration', () => {
  const originalLogLevel = process.env.LOG_LEVEL;
  const originalOneMcpLogLevel = process.env.ONE_MCP_LOG_LEVEL;
  let tempLogFile: string;
  beforeEach(async () => {
    // Clear environment variables
    delete process.env.LOG_LEVEL;
    delete process.env.ONE_MCP_LOG_LEVEL;
    // Create temp file for log file tests
    tempLogFile = path.join(tmpdir(), `test-log-${Date.now()}.log`);
  });
  afterEach(async () => {
    // Restore original environment variables
    if (originalLogLevel !== undefined) {
      process.env.LOG_LEVEL = originalLogLevel;
    }
    if (originalOneMcpLogLevel !== undefined) {
      process.env.ONE_MCP_LOG_LEVEL = originalOneMcpLogLevel;
    }
    // Clean up temp log file
    try {
      await fs.unlink(tempLogFile);
    } catch {
      // Ignore if file doesn't exist
    }
    vi.clearAllMocks();
  });
  describe('configureLogger', () => {
    it('should use CLI log level when provided', () => {
      const mockClear = vi.spyOn(logger, 'clear');
      const mockAdd = vi.spyOn(logger, 'add');
      configureLogger({
        logLevel: 'debug',
        transport: 'http',
      });
      expect(logger.level).toBe('debug');
      expect(mockClear).toHaveBeenCalled();
      expect(mockAdd).toHaveBeenCalledWith(
        expect.objectContaining({
          level: 'debug',
        }),
      );
    });
    it('should prefer ONE_MCP_LOG_LEVEL over LOG_LEVEL', () => {
      process.env.ONE_MCP_LOG_LEVEL = 'warn';
      process.env.LOG_LEVEL = 'debug';
      const mockClear = vi.spyOn(logger, 'clear');
      configureLogger({
        transport: 'http',
      });
      expect(logger.level).toBe('warn');
      expect(mockClear).toHaveBeenCalled();
    });
    it('should fallback to LOG_LEVEL with deprecation warning', () => {
      process.env.LOG_LEVEL = 'error';
      const mockWarn = vi.spyOn(logger, 'warn');
      const mockClear = vi.spyOn(logger, 'clear');
      configureLogger({
        transport: 'http',
      });
      expect(logger.level).toBe('error');
      expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('LOG_LEVEL environment variable is deprecated'));
      expect(mockClear).toHaveBeenCalled();
    });
    it('should default to info level when no log level is specified', () => {
      const mockClear = vi.spyOn(logger, 'clear');
      configureLogger({
        transport: 'http',
      });
      expect(logger.level).toBe('info');
      expect(mockClear).toHaveBeenCalled();
    });
    it('should configure file transport when log-file is specified', () => {
      const mockClear = vi.spyOn(logger, 'clear');
      const mockAdd = vi.spyOn(logger, 'add');
      configureLogger({
        logLevel: 'debug',
        logFile: tempLogFile,
        transport: 'http',
      });
      expect(mockClear).toHaveBeenCalled();
      expect(mockAdd).toHaveBeenCalledWith(expect.any(Object));
      expect(logger.level).toBe('debug');
    });
    it('should only add file transport for stdio mode with log-file', () => {
      const mockClear = vi.spyOn(logger, 'clear');
      const mockAdd = vi.spyOn(logger, 'add');
      configureLogger({
        logLevel: 'info',
        logFile: tempLogFile,
        transport: 'stdio',
      });
      expect(mockClear).toHaveBeenCalled();
      expect(mockAdd).toHaveBeenCalledTimes(1); // File only
      expect(logger.level).toBe('info');
    });
    it('should add both console and file transport for non-stdio mode with log-file', () => {
      const mockClear = vi.spyOn(logger, 'clear');
      const mockAdd = vi.spyOn(logger, 'add');
      configureLogger({
        logLevel: 'info',
        logFile: tempLogFile,
        transport: 'http',
      });
      expect(mockClear).toHaveBeenCalled();
      expect(mockAdd).toHaveBeenCalledTimes(2); // File + Console
      expect(logger.level).toBe('info');
    });
    it('should convert MCP log levels to Winston log levels correctly', () => {
      const testCases = [
        { mcp: 'debug', winston: 'debug' },
        { mcp: 'info', winston: 'info' },
        { mcp: 'notice', winston: 'info' },
        { mcp: 'warn', winston: 'warn' }, // Support both 'warn' and 'warning'
        { mcp: 'warning', winston: 'warn' },
        { mcp: 'error', winston: 'error' },
        { mcp: 'critical', winston: 'error' },
        { mcp: 'unknown', winston: 'info' }, // fallback
      ];
      testCases.forEach(({ mcp, winston: expectedWinston }) => {
        const mockClear = vi.spyOn(logger, 'clear');
        configureLogger({
          logLevel: mcp,
          transport: 'http',
        });
        expect(logger.level).toBe(expectedWinston);
        mockClear.mockRestore();
      });
    });
  });
  describe('environment variable priority', () => {
    it('should prefer CLI log level over environment variables', () => {
      process.env.ONE_MCP_LOG_LEVEL = 'warn';
      process.env.LOG_LEVEL = 'error';
      configureLogger({
        logLevel: 'debug', // CLI should win
        transport: 'http',
      });
      expect(logger.level).toBe('debug');
    });
    it('should prefer ONE_MCP_LOG_LEVEL over LOG_LEVEL', () => {
      process.env.ONE_MCP_LOG_LEVEL = 'warn';
      process.env.LOG_LEVEL = 'error';
      configureLogger({
        transport: 'http',
      });
      expect(logger.level).toBe('warn');
    });
    it('should show deprecation warning for LOG_LEVEL', () => {
      const mockWarn = vi.spyOn(logger, 'warn').mockImplementation(() => logger);
      process.env.LOG_LEVEL = 'error';
      configureLogger({
        transport: 'http',
      });
      expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('LOG_LEVEL environment variable is deprecated'));
      mockWarn.mockRestore();
    });
  });
  describe('MCP to Winston level conversion', () => {
    it('should convert MCP log levels to Winston log levels correctly', () => {
      const testCases = [
        { mcp: 'debug', winston: 'debug' },
        { mcp: 'info', winston: 'info' },
        { mcp: 'notice', winston: 'info' },
        { mcp: 'warn', winston: 'warn' }, // Support both 'warn' and 'warning'
        { mcp: 'warning', winston: 'warn' },
        { mcp: 'error', winston: 'error' },
        { mcp: 'critical', winston: 'error' },
        { mcp: 'unknown', winston: 'info' }, // fallback
      ];
      testCases.forEach(({ mcp, winston: expectedWinston }) => {
        configureLogger({
          logLevel: mcp,
          transport: 'http',
        });
        expect(logger.level).toBe(expectedWinston);
      });
    });
  });
});