import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { Logger, LogLevel, LogEntry } from '../../../src/utils/logger.js';
import { writeFile, appendFile, mkdir } from 'fs/promises';
import { existsSync } from 'fs';
// Mock fs modules
vi.mock('fs/promises');
vi.mock('fs');
describe('Logger', () => {
let logger: Logger;
let stderrWriteSpy: ReturnType<typeof vi.spyOn>;
let mockWriteFile: ReturnType<typeof vi.mocked>;
let mockAppendFile: ReturnType<typeof vi.mocked>;
let mockMkdir: ReturnType<typeof vi.mocked>;
let mockExistsSync: ReturnType<typeof vi.mocked>;
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks();
// Mock process.stderr.write - the logger uses stderr for MCP compatibility
stderrWriteSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
// Mock fs functions
mockWriteFile = vi.mocked(writeFile);
mockAppendFile = vi.mocked(appendFile);
mockMkdir = vi.mocked(mkdir);
mockExistsSync = vi.mocked(existsSync);
mockWriteFile.mockResolvedValue(undefined);
mockAppendFile.mockResolvedValue(undefined);
mockMkdir.mockResolvedValue(undefined);
mockExistsSync.mockReturnValue(true);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('initialization', () => {
it('should create logger with default settings', () => {
logger = new Logger();
expect(logger).toBeInstanceOf(Logger);
});
it('should create logger with custom log level', () => {
logger = new Logger('debug');
expect(logger).toBeInstanceOf(Logger);
});
it('should create logger with file logging enabled', () => {
logger = new Logger('info', {
logFile: '/tmp/test.log',
enableFile: true,
enableConsole: false
});
expect(logger).toBeInstanceOf(Logger);
});
it('should create log directory when file logging enabled', () => {
mockExistsSync.mockReturnValue(false);
logger = new Logger('info', {
logFile: '/tmp/logs/test.log',
enableFile: true
});
expect(mockMkdir).toHaveBeenCalled();
});
});
describe('log levels', () => {
beforeEach(() => {
logger = new Logger('debug');
});
it('should log debug messages', () => {
logger.debug('Debug message');
expect(stderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('DEBUG')
);
});
it('should log info messages', () => {
logger.info('Info message');
expect(stderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('INFO')
);
});
it('should log warn messages', () => {
logger.warn('Warning message');
expect(stderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('WARN')
);
});
it('should log error messages', () => {
logger.error('Error message');
expect(stderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('ERROR')
);
});
});
describe('log level filtering', () => {
it('should filter debug messages when level is info', () => {
logger = new Logger('info');
logger.debug('Debug message');
expect(stderrWriteSpy).not.toHaveBeenCalled();
});
it('should filter info messages when level is warn', () => {
logger = new Logger('warn');
logger.info('Info message');
expect(stderrWriteSpy).not.toHaveBeenCalled();
});
it('should filter warn messages when level is error', () => {
logger = new Logger('error');
logger.warn('Warning message');
expect(stderrWriteSpy).not.toHaveBeenCalled();
});
it('should allow error messages at error level', () => {
logger = new Logger('error');
logger.error('Error message');
expect(stderrWriteSpy).toHaveBeenCalled();
});
});
describe('structured logging', () => {
beforeEach(() => {
logger = new Logger('debug');
});
it('should log with additional data', () => {
const data = { userId: 123, action: 'login' };
logger.info('User action', data);
expect(stderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('INFO')
);
});
it('should log with source information', () => {
logger.info('Message', undefined, 'TestModule');
expect(stderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('[TestModule]')
);
});
it('should log with both data and source', () => {
const data = { key: 'value' };
logger.info('Message', data, 'TestModule');
expect(stderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('[TestModule]')
);
});
});
describe('file logging', () => {
beforeEach(() => {
logger = new Logger('info', {
logFile: '/tmp/test.log',
enableFile: true,
enableConsole: false
});
});
it('should write to file when file logging enabled', async () => {
logger.info('Test message');
// Wait for async file operations
await new Promise(resolve => setTimeout(resolve, 10));
expect(mockAppendFile).toHaveBeenCalledWith(
'/tmp/test.log',
expect.stringContaining('Test message'),
'utf-8'
);
});
it('should not log to console when console disabled', () => {
logger.info('Test message');
expect(stderrWriteSpy).not.toHaveBeenCalled();
});
});
describe('log entry creation', () => {
beforeEach(() => {
logger = new Logger('debug');
});
it('should create proper log entry structure', () => {
const data = { test: true };
logger.info('Test message', data, 'TestSource');
// Verify the log entry structure by checking stderr output
expect(stderrWriteSpy).toHaveBeenCalledWith(
expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z.*INFO.*\[TestSource\].*Test message/)
);
});
});
describe('error handling', () => {
beforeEach(() => {
logger = new Logger('info', {
logFile: '/tmp/test.log',
enableFile: true
});
});
it('should handle file write errors gracefully', async () => {
mockAppendFile.mockRejectedValue(new Error('File write error'));
// Should not throw
expect(() => logger.info('Test message')).not.toThrow();
// Wait for async operations
await new Promise(resolve => setTimeout(resolve, 10));
});
it('should handle directory creation errors gracefully', async () => {
mockMkdir.mockRejectedValue(new Error('Directory creation error'));
mockExistsSync.mockReturnValue(false);
// Should not throw during initialization
expect(() => new Logger('info', {
logFile: '/tmp/nonexistent/test.log',
enableFile: true
})).not.toThrow();
// Wait for async operations to complete
await new Promise(resolve => setTimeout(resolve, 10));
});
});
describe('performance', () => {
beforeEach(() => {
logger = new Logger('info');
});
it('should not process filtered log messages', () => {
const expensiveData = () => {
throw new Error('Should not be called');
};
// Debug message should be filtered at info level, so the function shouldn't be called
logger.debug('Debug message');
expect(stderrWriteSpy).not.toHaveBeenCalled();
});
});
describe('session tracking', () => {
beforeEach(() => {
logger = new Logger('debug');
});
it('should include session ID when provided', () => {
// This would require extending the logger to support session IDs
// For now, we'll test the basic functionality
logger.info('Session message');
expect(stderrWriteSpy).toHaveBeenCalled();
});
});
});