[grade=B urn:ump:3wvdd3yegylr7e2pzn6tebyq5bisf2awmss7p4j3uwpwsujtnwoq] DC-134/135/136: Shipdeck integration - data-driven login pages, deploy events, badge suppression

DC-134: /api/v1/auth/login-page serves a generic gated auto-login page for
any service registered in services.json without a curated flow (App Selector
installs, DC-131 git installs). Curated pages always win; unknown services
still 404; sanitizer keeps digits/hyphens (shipdeck-style ids); display
names HTML-escaped. Kills the sso-gate.js edit + restart per new install.

DC-135: shipdeck journal.jsonl tail worker (startShipdeckWorker) ingests
deploy/rollback lifecycle rows into the Security Center as
source_type=shipdeck (notice/success, error/failure via verify[] block).

DC-136: deploy-aware badge suppression - suppressDuringDeploy() before the
bridge call in /deploy and /rollback, clearDeploySuppression() in finally
(every exit path incl. rejected fetches), reference-counted for overlapping
deploys, 10-min TTL auto-expiry (HEALTH_DEPLOY_SUPPRESS_MAX_MS).

23 new tests across 4 suites; full suite 2923/2923 green.

Codex judge: C (r1) -> C (r2) -> B (r3) -> B zero-blockers (r4,
urn:ump:3wvdd3yegylr7e2pzn6tebyq5bisf2awmss7p4j3uwpwsujtnwoq).
This commit is contained in:
DashCaddy Polish Loop
2026-09-16 03:10:54 -07:00
parent 0dd8493f98
commit 11c719e635
10 changed files with 912 additions and 11 deletions
@@ -0,0 +1,130 @@
/**
* 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);
});
});