/ tests / utils / log-rotator.test.js
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  });