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.
43 lines
2.3 KiB
JavaScript
43 lines
2.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
|
|
import { DEFAULT_ALERT_RULES } from '../src/release-observability.js';
|
|
|
|
const runbookPath = new URL('../docs/RELEASE-OBSERVABILITY.md', import.meta.url);
|
|
const packagePath = new URL('../package.json', import.meta.url);
|
|
|
|
test('every default alert rule is inventoried in the runbook with owner, threshold, and severity', async () => {
|
|
const runbook = await readFile(runbookPath, 'utf8');
|
|
const headingSlugs = [...runbook.matchAll(/^#{2,4} (.+)$/gm)]
|
|
.map(match => match[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''));
|
|
for (const rule of DEFAULT_ALERT_RULES) {
|
|
const pattern = new RegExp(
|
|
`\\| \`${rule.id}\`\\s*\\| ${rule.severity}\\s*\\| ${rule.owner}\\s*\\|`,
|
|
);
|
|
assert.match(runbook, pattern, `runbook table lacks a conforming row for ${rule.id}`);
|
|
const anchor = rule.runbook.split('#')[1];
|
|
assert.ok(anchor && headingSlugs.includes(anchor), `runbook link for ${rule.id} does not resolve to a heading (${anchor})`);
|
|
}
|
|
});
|
|
|
|
test('runbook documents the outage drill anchor, manual fallback, and privacy boundary', async () => {
|
|
const runbook = await readFile(runbookPath, 'utf8');
|
|
assert.match(runbook, /^## Simulate a worker outage$/m);
|
|
assert.match(runbook, /^## Alert inventory$/m);
|
|
assert.match(runbook, /manual fallback/i);
|
|
for (const forbidden of ['photos or medical imagery', 'stool records', 'session identifiers', 'environment dumps']) {
|
|
assert.match(runbook, new RegExp(forbidden.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'i'));
|
|
}
|
|
const externalUrls = [...runbook.matchAll(/https?:\/\/(?!127\.0\.0\.1)[^\s)`\]]+/g)].map(match => match[0]);
|
|
assert.deepEqual(externalUrls, [], 'runbook must not reference non-loopback hosts');
|
|
});
|
|
|
|
test('package scripts wire observability tests into the suite and syntax gate', async () => {
|
|
const packageJson = JSON.parse(await readFile(packagePath, 'utf8'));
|
|
assert.match(packageJson.scripts.test, /tests\/release-observability\.test\.js/);
|
|
assert.match(packageJson.scripts.test, /tests\/release-dashboard\.test\.js/);
|
|
assert.match(packageJson.scripts.test, /tests\/release-dashboard-cli\.test\.js/);
|
|
assert.match(packageJson.scripts['check:syntax'], /node --check scripts\/release_dashboard\.mjs/);
|
|
});
|