/** * LogManager Tests * * Tests for the log directory and file path management. * Uses temporary directories to avoid polluting the real log directory. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdir, rm, writeFile, utimes } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { LogManager } from './manager.js'; describe('LogManager', () => { let testDir: string; let manager: LogManager; beforeEach(async () => { // Create a unique temp directory for each test testDir = join(tmpdir(), `cw-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); manager = new LogManager({ baseDir: testDir }); }); afterEach(async () => { // Clean up temp directory after each test try { await rm(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); describe('getBaseDir', () => { it('should return the configured base directory', () => { expect(manager.getBaseDir()).toBe(testDir); }); it('should use default directory when not configured', () => { const defaultManager = new LogManager(); expect(defaultManager.getBaseDir()).toContain('.cw'); expect(defaultManager.getBaseDir()).toContain('logs'); }); }); describe('ensureLogDir', () => { it('should create the base log directory', async () => { expect(existsSync(testDir)).toBe(false); await manager.ensureLogDir(); expect(existsSync(testDir)).toBe(true); }); it('should not error if directory already exists', async () => { await mkdir(testDir, { recursive: true }); expect(existsSync(testDir)).toBe(true); // Should not throw await manager.ensureLogDir(); expect(existsSync(testDir)).toBe(true); }); }); describe('ensureProcessDir', () => { it('should create the process-specific log directory', async () => { const processId = 'test-process-123'; const expectedDir = join(testDir, processId); expect(existsSync(expectedDir)).toBe(false); await manager.ensureProcessDir(processId); expect(existsSync(expectedDir)).toBe(true); }); it('should create nested directories if base does not exist', async () => { const processId = 'nested-process'; const expectedDir = join(testDir, processId); expect(existsSync(testDir)).toBe(false); await manager.ensureProcessDir(processId); expect(existsSync(testDir)).toBe(true); expect(existsSync(expectedDir)).toBe(true); }); }); describe('getProcessDir', () => { it('should return the correct path for a process', () => { const processId = 'my-process'; const expected = join(testDir, processId); expect(manager.getProcessDir(processId)).toBe(expected); }); }); describe('getLogPath', () => { it('should return correct path for stdout log', () => { const processId = 'proc-1'; const expected = join(testDir, processId, 'stdout.log'); expect(manager.getLogPath(processId, 'stdout')).toBe(expected); }); it('should return correct path for stderr log', () => { const processId = 'proc-2'; const expected = join(testDir, processId, 'stderr.log'); expect(manager.getLogPath(processId, 'stderr')).toBe(expected); }); }); describe('listLogs', () => { it('should return empty array if base directory does not exist', async () => { expect(existsSync(testDir)).toBe(false); const logs = await manager.listLogs(); expect(logs).toEqual([]); }); it('should return empty array if no log directories exist', async () => { await mkdir(testDir, { recursive: true }); const logs = await manager.listLogs(); expect(logs).toEqual([]); }); it('should return process IDs for existing log directories', async () => { // Create some process directories await mkdir(join(testDir, 'process-a'), { recursive: true }); await mkdir(join(testDir, 'process-b'), { recursive: true }); await mkdir(join(testDir, 'process-c'), { recursive: true }); const logs = await manager.listLogs(); expect(logs).toHaveLength(3); expect(logs).toContain('process-a'); expect(logs).toContain('process-b'); expect(logs).toContain('process-c'); }); it('should only return directories, not files', async () => { await mkdir(testDir, { recursive: true }); await mkdir(join(testDir, 'valid-process'), { recursive: true }); await writeFile(join(testDir, 'some-file.txt'), 'not a directory'); const logs = await manager.listLogs(); expect(logs).toEqual(['valid-process']); }); }); describe('cleanOldLogs', () => { it('should return 0 when retainDays is not configured', async () => { const managerNoRetain = new LogManager({ baseDir: testDir }); const removed = await managerNoRetain.cleanOldLogs(); expect(removed).toBe(0); }); it('should return 0 when no directories exist', async () => { const managerWithRetain = new LogManager({ baseDir: testDir, retainDays: 7 }); const removed = await managerWithRetain.cleanOldLogs(); expect(removed).toBe(0); }); it('should remove directories older than retainDays', async () => { const managerWithRetain = new LogManager({ baseDir: testDir, retainDays: 7 }); // Create an "old" directory const oldDir = join(testDir, 'old-process'); await mkdir(oldDir, { recursive: true }); // Set mtime to 10 days ago const tenDaysAgo = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000); await utimes(oldDir, tenDaysAgo, tenDaysAgo); // Create a "new" directory const newDir = join(testDir, 'new-process'); await mkdir(newDir, { recursive: true }); const removed = await managerWithRetain.cleanOldLogs(); expect(removed).toBe(1); expect(existsSync(oldDir)).toBe(false); expect(existsSync(newDir)).toBe(true); }); it('should use provided retainDays over config value', async () => { const managerWithRetain = new LogManager({ baseDir: testDir, retainDays: 30 }); // Create directory that is 10 days old const oldDir = join(testDir, 'process'); await mkdir(oldDir, { recursive: true }); const tenDaysAgo = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000); await utimes(oldDir, tenDaysAgo, tenDaysAgo); // With config (30 days), should NOT remove expect(await managerWithRetain.cleanOldLogs()).toBe(0); expect(existsSync(oldDir)).toBe(true); // With explicit 5 days, SHOULD remove expect(await managerWithRetain.cleanOldLogs(5)).toBe(1); expect(existsSync(oldDir)).toBe(false); }); }); });