// resource-monitor.js creates a Docker instance at module level. // On test machines without Docker, the constructor reads from non-existent files (returns defaults). const resourceMonitor = require('../resource-monitor'); beforeEach(() => { // Reset singleton state resourceMonitor.stats = new Map(); resourceMonitor.alerts = new Map(); resourceMonitor.lastAlerts = new Map(); resourceMonitor.monitoring = false; if (resourceMonitor.monitoringInterval) { clearInterval(resourceMonitor.monitoringInterval); resourceMonitor.monitoringInterval = null; } }); afterAll(() => { resourceMonitor.stop(); }); // Helper: create a stat entry function makeStat(cpu = 10, memory = 50, timestamp = new Date().toISOString()) { return { timestamp, cpu: { percent: cpu, usage: cpu * 1000 }, memory: { usage: memory * 1024 * 1024, limit: 1024 * 1024 * 1024, percent: memory, usageMB: memory, limitMB: 1024 }, network: { rxBytes: 0, txBytes: 0, rxMB: 0, txMB: 0 }, disk: { readBytes: 0, writeBytes: 0, readMB: 0, writeMB: 0 }, pids: 5 }; } describe('recordStats', () => { test('creates new entry for unknown container', () => { resourceMonitor.recordStats('c1', '/my-app', makeStat()); expect(resourceMonitor.stats.has('c1')).toBe(true); expect(resourceMonitor.stats.get('c1').history).toHaveLength(1); }); test('appends to existing history', () => { resourceMonitor.recordStats('c1', '/my-app', makeStat()); resourceMonitor.recordStats('c1', '/my-app', makeStat()); expect(resourceMonitor.stats.get('c1').history).toHaveLength(2); }); test('updates container name', () => { resourceMonitor.recordStats('c1', '/old-name', makeStat()); resourceMonitor.recordStats('c1', '/new-name', makeStat()); expect(resourceMonitor.stats.get('c1').name).toBe('/new-name'); }); }); describe('getCurrentStats', () => { test('returns null for unknown container', () => { expect(resourceMonitor.getCurrentStats('nonexistent')).toBeNull(); }); test('returns latest history entry', () => { const stat1 = makeStat(10); const stat2 = makeStat(50); resourceMonitor.recordStats('c1', '/app', stat1); resourceMonitor.recordStats('c1', '/app', stat2); expect(resourceMonitor.getCurrentStats('c1').cpu.percent).toBe(50); }); }); describe('getHistoricalStats', () => { test('returns empty array for unknown container', () => { expect(resourceMonitor.getHistoricalStats('nonexistent')).toEqual([]); }); test('filters by time window', () => { const recent = makeStat(10, 50, new Date().toISOString()); const old = makeStat(10, 50, new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString()); resourceMonitor.stats.set('c1', { name: '/app', history: [old, recent] }); const result = resourceMonitor.getHistoricalStats('c1', 24); expect(result).toHaveLength(1); expect(result[0]).toBe(recent); }); }); describe('getAggregatedStats', () => { test('returns null for unknown container', () => { expect(resourceMonitor.getAggregatedStats('nonexistent')).toBeNull(); }); test('returns null when no recent history', () => { const old = makeStat(10, 50, new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString()); resourceMonitor.stats.set('c1', { name: '/app', history: [old] }); expect(resourceMonitor.getAggregatedStats('c1', 24)).toBeNull(); }); test('calculates correct avg/min/max for CPU', () => { const now = new Date().toISOString(); resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now), makeStat(30, 50, now), makeStat(20, 50, now)] }); const agg = resourceMonitor.getAggregatedStats('c1', 24); expect(agg.cpu.avg).toBe(20); expect(agg.cpu.min).toBe(10); expect(agg.cpu.max).toBe(30); }); test('calculates correct avg/min/max for memory', () => { const now = new Date().toISOString(); resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 40, now), makeStat(10, 60, now), makeStat(10, 80, now)] }); const agg = resourceMonitor.getAggregatedStats('c1', 24); expect(agg.memory.avg).toBe(60); expect(agg.memory.min).toBe(40); expect(agg.memory.max).toBe(80); }); test('includes dataPoints and timeRange', () => { const now = new Date().toISOString(); resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] }); const agg = resourceMonitor.getAggregatedStats('c1', 24); expect(agg.dataPoints).toBe(1); expect(agg.timeRange).toBe(24); }); }); describe('checkAlerts', () => { test('does nothing when alert config is missing', () => { const handler = jest.fn(); resourceMonitor.on('alert', handler); resourceMonitor.checkAlerts('c1', '/app', makeStat(99)); expect(handler).not.toHaveBeenCalled(); resourceMonitor.removeListener('alert', handler); }); test('does nothing when alerts are disabled', () => { resourceMonitor.alerts.set('c1', { enabled: false, cpuThreshold: 50 }); const handler = jest.fn(); resourceMonitor.on('alert', handler); resourceMonitor.checkAlerts('c1', '/app', makeStat(99)); expect(handler).not.toHaveBeenCalled(); resourceMonitor.removeListener('alert', handler); }); test('triggers CPU alert when threshold exceeded', () => { resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 50, cooldownMinutes: 0 }); const handler = jest.fn(); resourceMonitor.on('alert', handler); resourceMonitor.checkAlerts('c1', '/app', makeStat(75)); expect(handler).toHaveBeenCalled(); const alertData = handler.mock.calls[0][0]; expect(alertData.alerts[0].type).toBe('cpu'); resourceMonitor.removeListener('alert', handler); }); test('triggers memory alert when threshold exceeded', () => { resourceMonitor.alerts.set('c1', { enabled: true, memoryThreshold: 70, cooldownMinutes: 0 }); const handler = jest.fn(); resourceMonitor.on('alert', handler); resourceMonitor.checkAlerts('c1', '/app', makeStat(10, 80)); expect(handler).toHaveBeenCalled(); const alertData = handler.mock.calls[0][0]; expect(alertData.alerts[0].type).toBe('memory'); resourceMonitor.removeListener('alert', handler); }); test('respects cooldown period', () => { resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 50, cooldownMinutes: 15 }); resourceMonitor.lastAlerts.set('c1', Date.now()); // Just alerted const handler = jest.fn(); resourceMonitor.on('alert', handler); resourceMonitor.checkAlerts('c1', '/app', makeStat(99)); expect(handler).not.toHaveBeenCalled(); resourceMonitor.removeListener('alert', handler); }); test('does not trigger when below threshold', () => { resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 90, cooldownMinutes: 0 }); const handler = jest.fn(); resourceMonitor.on('alert', handler); resourceMonitor.checkAlerts('c1', '/app', makeStat(50)); expect(handler).not.toHaveBeenCalled(); resourceMonitor.removeListener('alert', handler); }); }); describe('setAlertConfig / getAlertConfig / removeAlertConfig', () => { test('stores alert config', () => { resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 }); expect(resourceMonitor.alerts.has('c1')).toBe(true); }); test('retrieves stored config', () => { resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 }); const config = resourceMonitor.getAlertConfig('c1'); expect(config.cpuThreshold).toBe(80); }); test('returns null for non-existent config', () => { expect(resourceMonitor.getAlertConfig('nonexistent')).toBeNull(); }); test('removes config and last alert', () => { resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 }); resourceMonitor.lastAlerts.set('c1', Date.now()); resourceMonitor.removeAlertConfig('c1'); expect(resourceMonitor.alerts.has('c1')).toBe(false); expect(resourceMonitor.lastAlerts.has('c1')).toBe(false); }); }); describe('getAllStats', () => { test('returns empty object when no stats', () => { expect(resourceMonitor.getAllStats()).toEqual({}); }); test('includes current and aggregated for each container', () => { const now = new Date().toISOString(); resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] }); const all = resourceMonitor.getAllStats(); expect(all['c1']).toBeDefined(); expect(all['c1'].name).toBe('/app'); expect(all['c1'].current).toBeDefined(); expect(all['c1'].aggregated).toBeDefined(); }); }); describe('exportStats / importStats', () => { test('export returns object with stats and alerts', () => { const now = new Date().toISOString(); resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] }); resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 80 }); const exported = resourceMonitor.exportStats(); expect(exported.stats).toBeDefined(); expect(exported.alerts).toBeDefined(); expect(exported.exportedAt).toBeDefined(); }); test('import restores stats from backup', () => { const backup = { stats: { 'c1': { name: '/app', history: [makeStat()] } }, alerts: { 'c1': { enabled: true, cpuThreshold: 80 } } }; resourceMonitor.importStats(backup); expect(resourceMonitor.stats.has('c1')).toBe(true); expect(resourceMonitor.alerts.has('c1')).toBe(true); }); }); describe('cleanupOldStats', () => { test('removes entries older than retention period', () => { const old = makeStat(10, 50, new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString()); const recent = makeStat(10, 50, new Date().toISOString()); resourceMonitor.stats.set('c1', { name: '/app', history: [old, recent] }); resourceMonitor.cleanupOldStats(); expect(resourceMonitor.stats.get('c1').history).toHaveLength(1); }); test('deletes container entirely when no recent data', () => { const old = makeStat(10, 50, new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString()); resourceMonitor.stats.set('c1', { name: '/app', history: [old] }); resourceMonitor.cleanupOldStats(); expect(resourceMonitor.stats.has('c1')).toBe(false); }); }); describe('start / stop', () => { test('start sets monitoring flag', () => { jest.useFakeTimers(); resourceMonitor.start(); expect(resourceMonitor.monitoring).toBe(true); resourceMonitor.stop(); jest.useRealTimers(); }); test('stop clears interval', () => { jest.useFakeTimers(); resourceMonitor.start(); resourceMonitor.stop(); expect(resourceMonitor.monitoring).toBe(false); expect(resourceMonitor.monitoringInterval).toBeNull(); jest.useRealTimers(); }); test('start is idempotent', () => { jest.useFakeTimers(); resourceMonitor.start(); const first = resourceMonitor.monitoringInterval; resourceMonitor.start(); expect(resourceMonitor.monitoringInterval).toBe(first); resourceMonitor.stop(); jest.useRealTimers(); }); });