log-rotator.test.js
1 /** 2 * Log Rotator Tests 3 * 4 * Tests for rotateLogs(). Uses real /tmp filesystem to verify log rotation behavior. 5 */ 6 7 import { test, describe, before, after } from 'node:test'; 8 import assert from 'node:assert/strict'; 9 import { mkdirSync, writeFileSync, existsSync, rmSync, utimesSync, readdirSync } from 'fs'; 10 import { join } from 'path'; 11 12 import { rotateLogs } from '../../src/utils/log-rotator.js'; 13 14 // ─── Test setup ──────────────────────────────────────────────────────────────── 15 16 const TEST_LOG_DIR = `/tmp/test-log-rotator-${process.pid}`; 17 18 function createLogFile(filename, ageDays, sizeBytes = 1024) { 19 const filePath = join(TEST_LOG_DIR, filename); 20 writeFileSync(filePath, 'x'.repeat(sizeBytes)); 21 // Set mtime to simulate age 22 const ageMs = ageDays * 24 * 60 * 60 * 1000; 23 const mtime = new Date(Date.now() - ageMs); 24 utimesSync(filePath, mtime, mtime); 25 return filePath; 26 } 27 28 before(() => { 29 mkdirSync(TEST_LOG_DIR, { recursive: true }); 30 }); 31 32 after(() => { 33 rmSync(TEST_LOG_DIR, { recursive: true, force: true }); 34 }); 35 36 // ─── rotateLogs ─────────────────────────────────────────────────────────────── 37 38 describe('rotateLogs', () => { 39 test('returns zero stats when log directory does not exist', () => { 40 const result = rotateLogs({ logDir: '/tmp/nonexistent-dir-12345' }); 41 assert.equal(result.deleted, 0); 42 assert.equal(result.kept, 0); 43 assert.equal(result.freedSpace, 0); 44 }); 45 46 test('returns zero deleted when all files are within retention window', () => { 47 // Files 1 and 2 days old, retention=7 48 createLogFile('recent-1.log', 1); 49 createLogFile('recent-2.log', 2); 50 51 const result = rotateLogs({ logDir: TEST_LOG_DIR, retentionDays: 7 }); 52 assert.equal(result.deleted, 0); 53 assert.ok(result.kept >= 2, `Should keep recent files, kept: ${result.kept}`); 54 55 // Cleanup 56 rmSync(join(TEST_LOG_DIR, 'recent-1.log'), { force: true }); 57 rmSync(join(TEST_LOG_DIR, 'recent-2.log'), { force: true }); 58 }); 59 60 test('deletes files older than retention window', () => { 61 const oldFile = createLogFile('old-log.log', 10, 2048); 62 createLogFile('new-log.log', 1); 63 64 const result = rotateLogs({ logDir: TEST_LOG_DIR, retentionDays: 7 }); 65 assert.ok(result.deleted >= 1, `Should have deleted at least 1 file, got: ${result.deleted}`); 66 assert.ok(!existsSync(oldFile), 'Old log file should be deleted'); 67 68 // Cleanup remaining 69 rmSync(join(TEST_LOG_DIR, 'new-log.log'), { force: true }); 70 }); 71 72 test('dry run does not delete files', () => { 73 const oldFile = createLogFile('dry-run-old.log', 15, 512); 74 75 const result = rotateLogs({ logDir: TEST_LOG_DIR, retentionDays: 7, dryRun: true }); 76 assert.equal(result.deleted, 0, 'Dry run should not delete anything'); 77 assert.ok(existsSync(oldFile), 'Old file should still exist after dry run'); 78 79 // Cleanup 80 rmSync(oldFile, { force: true }); 81 }); 82 83 test('only processes .log files (ignores other extensions)', () => { 84 writeFileSync(join(TEST_LOG_DIR, 'ignored.txt'), 'text'); 85 writeFileSync(join(TEST_LOG_DIR, 'ignored.json'), '{}'); 86 writeFileSync(join(TEST_LOG_DIR, 'ignored.md'), '# readme'); 87 createLogFile('shouldcount.log', 1); 88 89 const result = rotateLogs({ logDir: TEST_LOG_DIR, retentionDays: 7 }); 90 // Kept count should only count .log files 91 assert.ok(result.kept >= 1, 'Should count .log files'); 92 93 // Cleanup 94 rmSync(join(TEST_LOG_DIR, 'ignored.txt'), { force: true }); 95 rmSync(join(TEST_LOG_DIR, 'ignored.json'), { force: true }); 96 rmSync(join(TEST_LOG_DIR, 'ignored.md'), { force: true }); 97 rmSync(join(TEST_LOG_DIR, 'shouldcount.log'), { force: true }); 98 }); 99 100 test('freedSpace reflects actual deleted file sizes', () => { 101 const size = 4096; 102 createLogFile('sized-old.log', 20, size); 103 104 const result = rotateLogs({ logDir: TEST_LOG_DIR, retentionDays: 7 }); 105 assert.ok( 106 result.freedSpace >= size, 107 `freedSpace should be >= ${size}, got ${result.freedSpace}` 108 ); 109 }); 110 111 test('uses default retention of 7 days when not specified', () => { 112 createLogFile('default-test.log', 8, 100); // 8 days > 7 day default 113 const before = readdirSync(TEST_LOG_DIR).filter(f => f.endsWith('.log')).length; 114 115 const result = rotateLogs({ logDir: TEST_LOG_DIR }); 116 // Should have deleted the 8-day-old file with default 7-day retention 117 assert.ok(result.deleted >= 0, 'Should return stats'); 118 }); 119 120 test('returns correct shape', () => { 121 const result = rotateLogs({ logDir: '/tmp/nonexistent-dir-999' }); 122 assert.ok('deleted' in result); 123 assert.ok('kept' in result); 124 assert.ok('freedSpace' in result); 125 }); 126 });