/** * Smoke tests for log-digest.js * Verifies the singleton LogDigest exposes the expected interface, parses * Docker multiplexed log streams, formats digests, and supports on-demand * daily digest generation with mocked Docker. */ const fsReal = require('fs'); const os = require('os'); const path = require('path'); jest.mock('dockerode', () => { const listContainers = jest.fn().mockResolvedValue([]); const getContainer = jest.fn(() => ({ logs: jest.fn().mockResolvedValue(Buffer.from([])), })); function Docker() {} Docker.prototype.listContainers = listContainers; Docker.prototype.getContainer = getContainer; return Docker; }); jest.mock('fs', () => { const actual = jest.requireActual('fs'); return { ...actual, existsSync: jest.fn().mockReturnValue(true), mkdirSync: jest.fn(), }; }); jest.mock('../docker-maintenance', () => ({ getDiskUsage: jest.fn().mockResolvedValue(null), })); const Docker = require('dockerode'); const fs = require('fs'); const logDigest = require('../log-digest'); describe('LogDigest (singleton)', () => { let dockerInstance; let tempDir; beforeEach(() => { // Each test gets a fresh Docker() mock instance jest.clearAllMocks(); fs.existsSync.mockReturnValue(true); // Use a real, writable temp directory so writeFile inside generateDailyDigest // does not blow up. Each test gets a fresh dir to avoid cross-test pollution. tempDir = fsReal.mkdtempSync(path.join(os.tmpdir(), 'dc-digest-test-')); logDigest.hourlySummaries = []; logDigest.lastCollect = null; logDigest.running = false; logDigest.digestDir = null; if (logDigest.collectInterval) { clearInterval(logDigest.collectInterval); logDigest.collectInterval = null; } if (logDigest.digestTimeout) { clearTimeout(logDigest.digestTimeout); logDigest.digestTimeout = null; } dockerInstance = new Docker(); }); afterEach(() => { logDigest.stop(); if (tempDir && fsReal.existsSync(tempDir)) { fsReal.rmSync(tempDir, { recursive: true, force: true }); } }); test('is an EventEmitter and exposes the documented API', () => { expect(typeof logDigest.on).toBe('function'); expect(typeof logDigest.emit).toBe('function'); expect(typeof logDigest.start).toBe('function'); expect(typeof logDigest.stop).toBe('function'); expect(typeof logDigest.generateDailyDigest).toBe('function'); expect(typeof logDigest.getLatestDigest).toBe('function'); expect(typeof logDigest.getDigestByDate).toBe('function'); expect(typeof logDigest.getDigestText).toBe('function'); expect(typeof logDigest.listDigests).toBe('function'); expect(typeof logDigest.getLiveData).toBe('function'); expect(typeof logDigest.getStatus).toBe('function'); }); test('getStatus returns current state', () => { const status = logDigest.getStatus(); expect(status).toEqual({ running: false, lastCollect: null, hourlySummaries: 0, digestDir: null, }); }); test('start sets running and digestDir', () => { logDigest.start(tempDir); expect(logDigest.running).toBe(true); expect(logDigest.digestDir).toBe(tempDir); }); test('start is idempotent — second call does nothing new', () => { logDigest.start(tempDir); const firstInterval = logDigest.collectInterval; logDigest.start(tempDir); expect(logDigest.collectInterval).toBe(firstInterval); }); test('_parseDockerLogs decodes multiplexed log frames into lines', () => { // Stream type byte: 0=stdin, 1=stdout, 2=stderr // Header: [type, 0, 0, 0, size-BE-uint32] function frame(streamType, text) { const buf = Buffer.from(text, 'utf8'); const header = Buffer.alloc(8); header[0] = streamType; header.writeUInt32BE(buf.length, 4); return Buffer.concat([header, buf]); } const multiplexed = Buffer.concat([ frame(1, 'hello world\n'), frame(2, '2026-03-13T12:00:00.000Z an error happened\n'), ]); const lines = logDigest._parseDockerLogs(multiplexed); expect(lines).toHaveLength(2); expect(lines[0]).toEqual({ stream: 'stdout', text: 'hello world', timestamp: null, }); expect(lines[1].stream).toBe('stderr'); expect(lines[1].text).toBe('an error happened'); expect(lines[1].timestamp).toBe('2026-03-13T12:00:00'); }); test('generateDailyDigest with empty summaries produces minimal digest', async () => { logDigest.start(tempDir); const digest = await logDigest.generateDailyDigest('2099-01-01'); expect(digest.date).toBe('2099-01-01'); expect(digest.services).toEqual({}); expect(digest.summary.totalServices).toBe(0); expect(digest.summary.totalErrors).toBe(0); expect(Array.isArray(digest.notableEvents)).toBe(true); // Confirm the file was actually written const writtenPath = path.join(tempDir, 'digest-2099-01-01.log'); expect(fsReal.existsSync(writtenPath)).toBe(true); const jsonPath = path.join(tempDir, 'digest-2099-01-01.json'); expect(fsReal.existsSync(jsonPath)).toBe(true); }); test('getLiveData returns shape with date, hoursCollected, services', () => { const data = logDigest.getLiveData(); expect(data).toHaveProperty('date'); expect(data).toHaveProperty('hoursCollected'); expect(data).toHaveProperty('services'); expect(data).toHaveProperty('lastCollect'); }); test('getLatestDigest returns null when digestDir is null', async () => { logDigest.digestDir = null; const result = await logDigest.getLatestDigest(); expect(result).toBeNull(); }); test('getDigestByDate returns null when no file exists', async () => { logDigest.digestDir = '/nonexistent/path'; const result = await logDigest.getDigestByDate('2020-01-01'); expect(result).toBeNull(); }); test('listDigests returns empty array when digestDir is null', async () => { logDigest.digestDir = null; const result = await logDigest.listDigests(); expect(result).toEqual([]); }); test('stop clears intervals and timeouts', () => { logDigest.start(tempDir); logDigest.stop(); expect(logDigest.running).toBe(false); expect(logDigest.collectInterval).toBeNull(); expect(logDigest.digestTimeout).toBeNull(); }); });