// tests/kb-update/test-log-rotate.test.mjs // Unit tests for scripts/kb-update/lib/log-rotate.mjs import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync, statSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { rotateLog } from '../../scripts/kb-update/lib/log-rotate.mjs'; function withTmp(fn) { const dir = mkdtempSync(join(tmpdir(), 'lr-test-')); try { return fn(dir); } finally { rmSync(dir, { recursive: true, force: true }); } } test('rotateLog — missing log is a no-op', () => { withTmp((dir) => { const log = join(dir, 'kb.log'); const result = rotateLog(log, { maxSizeBytes: 100 }); assert.equal(result.rotated, false); assert.equal(existsSync(log), false); }); }); test('rotateLog — under-size log is not rotated', () => { withTmp((dir) => { const log = join(dir, 'kb.log'); writeFileSync(log, 'tiny', 'utf8'); const before = statSync(log).size; const result = rotateLog(log, { maxSizeBytes: 1024 }); assert.equal(result.rotated, false); assert.equal(statSync(log).size, before); assert.equal(existsSync(`${log}.1`), false); }); }); test('rotateLog — over-size log moves to .1 with original content', () => { withTmp((dir) => { const log = join(dir, 'kb.log'); const content = 'x'.repeat(2048); writeFileSync(log, content, 'utf8'); const result = rotateLog(log, { maxSizeBytes: 1024 }); assert.equal(result.rotated, true); assert.equal(existsSync(log), false); assert.equal(existsSync(`${log}.1`), true); assert.equal(readFileSync(`${log}.1`, 'utf8'), content); }); }); test('rotateLog — chained rotation across 6 calls keeps at most maxGenerations files', () => { withTmp((dir) => { const log = join(dir, 'kb.log'); const max = 5; // 1 active + 4 rotated; lastGen = 4 for (let i = 0; i < 6; i++) { writeFileSync(log, `gen-${i}-${'x'.repeat(2048)}`, 'utf8'); rotateLog(log, { maxSizeBytes: 1024, maxGenerations: max }); } // After 6 over-size rotations, keep .1..(max-1) = .1..4 from the latest // chain. Active log was just rotated → no active log on disk. assert.equal(existsSync(log), false); for (let i = 1; i <= max - 1; i++) { assert.equal(existsSync(`${log}.${i}`), true, `expected ${log}.${i}`); } assert.equal(existsSync(`${log}.${max}`), false); assert.equal(existsSync(`${log}.${max + 1}`), false); }); }); test('rotateLog — existing oldest generation is dropped on next rotation', () => { withTmp((dir) => { const log = join(dir, 'kb.log'); // Pre-seed an oldest generation so we can prove it gets dropped. writeFileSync(`${log}.4`, 'oldest-content', 'utf8'); writeFileSync(log, 'x'.repeat(2048), 'utf8'); const result = rotateLog(log, { maxSizeBytes: 1024, maxGenerations: 5 }); assert.equal(result.rotated, true); // .4 dropped; new .1 created from prior active; nothing at .4 since // there were no .3/.2 to shift up. assert.equal(result.dropped, `${log}.4`); assert.equal(existsSync(`${log}.4`), false); assert.equal(existsSync(`${log}.1`), true); }); }); test('rotateLog — maxGenerations=1 keeps only the active slot (no .1)', () => { withTmp((dir) => { const log = join(dir, 'kb.log'); writeFileSync(log, 'x'.repeat(2048), 'utf8'); const result = rotateLog(log, { maxSizeBytes: 1024, maxGenerations: 1 }); assert.equal(result.rotated, true); assert.equal(existsSync(log), false); assert.equal(existsSync(`${log}.1`), false); assert.equal(result.dropped, log); }); }); test('rotateLog — preserves rotated content across two-step shift', () => { withTmp((dir) => { const log = join(dir, 'kb.log'); // First rotation writeFileSync(log, `first-${'x'.repeat(2048)}`, 'utf8'); rotateLog(log, { maxSizeBytes: 1024, maxGenerations: 3 }); assert.equal(readFileSync(`${log}.1`, 'utf8').startsWith('first-'), true); // Second rotation: prior .1 should shift to .2; new .1 from second active. writeFileSync(log, `second-${'x'.repeat(2048)}`, 'utf8'); rotateLog(log, { maxSizeBytes: 1024, maxGenerations: 3 }); assert.equal(readFileSync(`${log}.1`, 'utf8').startsWith('second-'), true); assert.equal(readFileSync(`${log}.2`, 'utf8').startsWith('first-'), true); }); }); test('rotateLog — empty logPath rejected', () => { assert.throws(() => rotateLog(''), /logPath is required/); assert.throws(() => rotateLog(undefined), /logPath is required/); });