/** * DC-136: deploy-aware badge suppression in the health checker. * * A shipdeck deploy/rollback restarts the target unit; probes that land * during that window blackhole (timeouts / 5xx) and — before this change — * flipped the badge red and opened outage incidents for what is routine * deploy noise. * * Pins: * - suppressDuringDeploy() holds the displayed badge through down probes * (even past DOWN_THRESHOLD) and emits nothing. * - Raw history keeps every probe (full fidelity preserved). * - checkForIncidents opens no outage/slow-response incident while * suppressed. * - After expiry the checker behaves exactly as before (down probes flip * the badge again). * - TTL is clamped to HEALTH_DEPLOY_SUPPRESS_MAX_MS. */ 'use strict'; const path = require('path'); const fs = require('fs'); const os = require('os'); const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'dc136-deploysuppress-')); process.env.HEALTH_DATA_DIR = TMP_DIR; process.env.HEALTH_CONFIG_FILE = path.join(TMP_DIR, 'health-config.json'); process.env.HEALTH_HISTORY_FILE = path.join(TMP_DIR, 'health-history.json'); process.env.HEALTH_DEPLOY_SUPPRESS_MAX_MS = '60000'; // test-visible clamp ceiling // Module exports a singleton instance — same pattern as // health-checker-hysteresis.test.js. const healthCheckerSingleton = require('../src/monitoring/health-checker'); function makeUp(serviceId = 'svc1') { return { serviceId, timestamp: new Date().toISOString(), status: 'up', responseTime: 50, statusCode: 200, message: 'Service is healthy', details: { headers: {}, bodyLength: 12 }, }; } function makeDown(serviceId = 'svc1') { return { serviceId, timestamp: new Date().toISOString(), status: 'down', responseTime: 50, statusCode: 500, message: 'fail', details: { headers: {}, bodyLength: 0 }, }; } describe('DC-136: deploy suppression on the dashboard badge', () => { let hc; let emitSpy; beforeEach(() => { hc = healthCheckerSingleton; hc.displayedStatus = new Map(); hc.consecutiveSinceChange = new Map(); hc.currentStatus = new Map(); hc.history = {}; hc.deploySuppressedUntil = new Map(); hc.deploySuppressRefs = new Map(); // judge r4 polish: reset ref counts too hc.incidents = []; emitSpy = jest.spyOn(hc, 'emit'); }); afterEach(() => { emitSpy.mockRestore(); }); test('down probes during the suppress window do not flip the badge', () => { hc.recordStatus('svc1', makeUp()); expect(hc.displayedStatus.get('svc1').status).toBe('up'); hc.suppressDuringDeploy('svc1'); hc.recordStatus('svc1', makeDown()); hc.recordStatus('svc1', makeDown()); hc.recordStatus('svc1', makeDown()); // well past DOWN_THRESHOLD=2 expect(hc.displayedStatus.get('svc1').status).toBe('up'); const statusEmits = emitSpy.mock.calls.filter(c => c[0] === 'status-check'); expect(statusEmits.length).toBe(1); // only the bootstrap "up" emit }); test('raw history keeps every probe during suppression', () => { hc.recordStatus('svc1', makeUp()); hc.suppressDuringDeploy('svc1'); hc.recordStatus('svc1', makeDown()); hc.recordStatus('svc1', makeDown()); expect(hc.history.svc1.length).toBe(3); expect(hc.currentStatus.get('svc1').status).toBe('down'); }); test('no outage or slow-response incidents open while suppressed', () => { hc.recordStatus('svc1', makeUp()); hc.suppressDuringDeploy('svc1'); const down = makeDown(); down.responseTime = 99999; // would trip slow-response too hc.recordStatus('svc1', down); expect(hc.incidents.length).toBe(0); }); test('after expiry, down probes flip the badge again (unchanged semantics)', () => { hc.recordStatus('svc1', makeUp()); hc.suppressDuringDeploy('svc1', 1); // expires immediately // spin clock past expiry without sleeps hc.deploySuppressedUntil.set('svc1', Date.now() - 1); hc.recordStatus('svc1', makeDown()); hc.recordStatus('svc1', makeDown()); expect(hc.displayedStatus.get('svc1').status).toBe('down'); }); test('ttl is clamped to HEALTH_DEPLOY_SUPPRESS_MAX_MS', () => { hc.suppressDuringDeploy('svc1', 10 * 60 * 60 * 1000); // 1h request const until = hc.deploySuppressedUntil.get('svc1'); expect(until - Date.now()).toBeLessThanOrEqual(60000 + 50); }); });