All checks were successful
Quality gates / quality (pull_request) Successful in 1m44s
Implements issue #41 acceptance criteria without touching the live host: - src/release-observability.js: sanitizeEvidence() allowlists bounded, privacy-safe evidence (release tag, commit, UTC time, per-check id/boundary/status/failure-class/latency/counters) and drops session identifiers, credentials, environment dumps, photo payloads, base64, image hashes, note text, emails, and any oversized/suspicious value. - buildDashboard(): app/api/queue/model boundary rollups, failure-class counts, manual-fallback state; evaluateAlerts() with owner, threshold, severity, and runbook anchor per rule. - scripts/release_dashboard.mjs: local operator CLI over sanitized evidence files or a loopback drill fixture; never contacts a live host and exits nonzero without echoing rejected input. - docs/RELEASE-OBSERVABILITY.md: alert inventory, evidence schema, simulated worker-outage drill, incident flow, privacy boundary. - Tests: sanitizer hostile-payload coverage, dashboard/alert rules, end-to-end loopback outage drill asserting exactly one actionable page alert plus graceful manual fallback, runbook/package contract.
138 lines
5.8 KiB
JavaScript
138 lines
5.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawn } from 'node:child_process';
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import http from 'node:http';
|
|
|
|
const root = fileURLToPath(new URL('..', import.meta.url));
|
|
let nextPort = 43600;
|
|
|
|
const healthyEvidence = {
|
|
schemaVersion: 1,
|
|
releaseTag: 'daily-2026-08-22.1',
|
|
commit: 'ca31e6d38bec649407f63880504554c59f2878ae',
|
|
generatedAtUtc: '2026-08-22T12:00:00Z',
|
|
checks: [
|
|
{ id: 'app.healthz', boundary: 'app', status: 'pass', latencyMs: 12 },
|
|
{ id: 'api.analyze', boundary: 'api', status: 'pass', counters: { ok: 40, fail: 0 } },
|
|
{ id: 'queue.depth', boundary: 'queue', status: 'pass', counters: { depth: 2, ok: 30, fail: 0 } },
|
|
{ id: 'model.inference', boundary: 'model', status: 'pass', latencyMs: 900, counters: { ok: 25, fail: 0, abstain: 3 } },
|
|
],
|
|
};
|
|
|
|
function runCli(args) {
|
|
return new Promise(resolve => {
|
|
const child = spawn(process.execPath, ['scripts/release_dashboard.mjs', ...args], {
|
|
cwd: root,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: process.env,
|
|
});
|
|
let stdout = '';
|
|
let stderr = '';
|
|
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
const timer = setTimeout(() => child.kill('SIGKILL'), 15_000);
|
|
child.on('close', code => {
|
|
clearTimeout(timer);
|
|
resolve({ code, stdout, stderr });
|
|
});
|
|
});
|
|
}
|
|
|
|
test('cli renders boundary dashboard with zero alerts from sanitized evidence file', async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), 'timmy-obs-'));
|
|
try {
|
|
const evidencePath = join(dir, 'evidence.json');
|
|
await writeFile(evidencePath, JSON.stringify(healthyEvidence));
|
|
|
|
const result = await runCli(['--evidence', evidencePath]);
|
|
assert.equal(result.code, 0, result.stderr);
|
|
assert.match(result.stdout, /RELEASE OBSERVABILITY DASHBOARD/);
|
|
assert.match(result.stdout, /release daily-2026-08-22\.1 · commit ca31e6d38bec/);
|
|
assert.match(result.stdout, /app\s+1\/1 pass/);
|
|
assert.match(result.stdout, /queue\s+1\/1 pass/);
|
|
assert.match(result.stdout, /model\s+1\/1 pass/);
|
|
assert.match(result.stdout, /manual fallback: available/);
|
|
assert.match(result.stdout, /alerts \(0\)/);
|
|
assert.doesNotMatch(result.stdout, /token|cookie|session|password/i);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('cli exits nonzero on unsanitizable evidence and never echoes its contents', async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), 'timmy-obs-'));
|
|
try {
|
|
const hostile = {
|
|
...healthyEvidence,
|
|
sessionToken: 'sess_live_supersecret99',
|
|
checks: [{ id: 'x', boundary: 'nope-boundary', status: 'weird' }],
|
|
};
|
|
const evidencePath = join(dir, 'hostile.json');
|
|
await writeFile(evidencePath, JSON.stringify(hostile));
|
|
|
|
const result = await runCli(['--evidence', evidencePath]);
|
|
assert.equal(result.code, 2);
|
|
assert.doesNotMatch(result.stderr + result.stdout, /supersecret/);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
async function startDrillServer() {
|
|
const port = nextPort++;
|
|
const origin = `http://127.0.0.1:${port}`;
|
|
let workerUp = true;
|
|
const app = http.createServer((req, res) => {
|
|
const url = new URL(req.url, origin);
|
|
if (url.pathname === '/drill/outage' && req.method === 'POST') {
|
|
workerUp = false;
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ ok: true }));
|
|
return;
|
|
}
|
|
if (url.pathname === '/api/healthz') {
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ ok: true, release: 'daily-2026-08-22.1', commit: 'ca31e6d38bec649407f63880504554c59f2878ae', visionEnabled: false, agentEnabled: false }));
|
|
return;
|
|
}
|
|
if (url.pathname === '/api/drill/checks') {
|
|
const checks = workerUp
|
|
? [
|
|
{ id: 'api.analyze', boundary: 'api', status: 'pass', counters: { ok: 5, fail: 0 } },
|
|
{ id: 'queue.depth', boundary: 'queue', status: 'pass', counters: { depth: 1, ok: 5, fail: 0 } },
|
|
{ id: 'model.inference', boundary: 'model', status: 'pass', latencyMs: 800, counters: { ok: 4, fail: 0 } },
|
|
]
|
|
: [
|
|
{ id: 'api.analyze', boundary: 'api', status: 'fail', failureClass: 'worker.unavailable' },
|
|
{ id: 'queue.depth', boundary: 'queue', status: 'fail', failureClass: 'worker.unavailable', counters: { depth: 17 } },
|
|
{ id: 'model.inference', boundary: 'model', status: 'fail', failureClass: 'worker.unavailable', counters: { timeout: 4 } },
|
|
];
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ checks }));
|
|
return;
|
|
}
|
|
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
res.end('Not found');
|
|
});
|
|
await new Promise(resolve => app.listen(port, '127.0.0.1', resolve));
|
|
return { origin, close: () => new Promise(resolve => app.close(() => resolve())) };
|
|
}
|
|
|
|
test('outage drill flips one simulated switch and reports exactly one page alert plus manual fallback', async t => {
|
|
const server = await startDrillServer();
|
|
t.after(() => server.close());
|
|
|
|
const result = await runCli(['--drill-origin', server.origin]);
|
|
assert.equal(result.code, 0, result.stderr);
|
|
assert.match(result.stdout, /WORKER OUTAGE DRILL/);
|
|
assert.match(result.stdout, /pre-drill: 0 alerts/i);
|
|
assert.match(result.stdout, /post-outage: exactly 1 alert/i);
|
|
assert.match(result.stdout, /PAGE worker\.unavailable on daily-2026-08-22\.1/);
|
|
assert.match(result.stdout, /manual fallback: AVAILABLE — local journal remains usable/);
|
|
assert.match(result.stdout, /DRILL PASS/);
|
|
});
|