/** * The recurrence gate (celilo#1326, part two). * * D7's rule: every claim celilo makes about the fleet is measured against the * thing it describes, or reported as `unmeasured`. There is no third option, * because contributing nothing renders the category green and the verdict * READY — the defect shape #1326 exists to close. * * A category can silently violate that rule three ways: someone adds a new * DriftCategory with no unmeasured path, someone rewires a category's * "could not reach" case to invent a finding of the wrong severity, or a * refactor drops the path entirely. This gate catches all three by walking * EVERY category — computed from the type, not from a list someone maintains * by hand — and demanding each either: * * - `probed`: its audit function, given deps that cannot reach their * subject, produces at least one `unmeasured` finding; or * - `allow-listed`: the category structurally always reaches its subject, * with the reason recorded here. * * `Record` is the enforcement: a category added to * the union without a gate entry is a type error, and the author has to * answer the question this test exists to ask. * * WATCHING IT FAIL (Rule 7.6): revert ce-prr5's `health.ts` change (the * `no-checks` branch in auditHealth) and the `health` entry fails — health * produces nothing for a module with no hook, which is exactly how six * modules rendered READY while never having been measured. */ import { describe, expect, test } from 'bun:test'; import { ALL_CATEGORIES } from '../../cli/tui/audit-state'; import type { DbClient } from '../../db/client'; import { auditCliVersion } from './cli-version'; import { auditDiskSpace } from './disk-space'; import { auditHealth } from './health'; import { auditInterfaceClassification } from './interface-classification'; import { auditModuleIntegrity } from './module-integrity'; import { auditModuleVersions } from './module-versions'; import { auditPublicDns } from './public-dns'; import { auditSchema } from './schema'; import { auditTerraformPlan } from './terraform-plan'; import { auditTrustedSources } from './trusted-sources'; import type { DriftCategory, DriftFinding, DriftSeverity } from './types'; type GateEntry = { /** The probe: call the category's audit with deps that cannot reach their subject. */ readonly probe?: () => Promise | DriftFinding[]; /** Why the category structurally always reaches its subject. */ readonly because?: string; }; const GATE: Record = { cli_version: { probe: () => auditCliVersion({ installedVersion: '9.9.9', fetcher: async () => null }), because: 'probed: the npm registry can be unreachable, and that is unmeasured', }, schema: { probe: () => auditSchema({ journal: () => null, applied: () => [], db: {} as DbClient, }), because: 'probed: a missing migration journal means nothing was compared', }, capability_abi: { because: 'compares capability versions claimed in installed module manifests against the framework registry and deployed providers, all read from celilo own DB and local manifests — the subject is celilo itself and is always reachable', }, browser_pin: { because: 'compares a bundled client playwright revision against the locally provisioned browser binary, both on this machine — always reachable', }, terraform_plan: { probe: () => auditTerraformPlan({ modules: [{ id: 'm', terraformDir: '/tmp/does-not-matter', envVars: {} }], run: async () => ({ exitCode: 0, stdout: '', stderr: '' }), }), because: 'probed: terraform exiting zero with an unreadable summary is not a clean plan', }, module_versions: { probe: () => auditModuleVersions({ installed: [{ id: 'm', version: '1.0.0' }], fetcher: () => Promise.reject(new Error('registry unreachable')), }), because: 'probed: a registry that did not answer is not a version difference', }, module_configs: { because: 'walks manifest variables against config rows in celilo DB — the subject is celilo itself and is always reachable', }, health: { probe: () => auditHealth({ results: [{ moduleId: 'm', status: 'no-checks', checks: [] }] }), because: 'probed: a module with no hook, or whose checks all skip, has measured nothing', }, backups: { because: 'reads recorded backup outcomes and effective cadences from celilo DB — the subject is celilo itself and is always reachable; a missing backup is drift, not silence', }, abandoned_operations: { because: 'counts reclaimed operation rows in celilo DB — always reachable', }, undeployed_modules: { because: 'reads module lifecycle states from celilo DB — always reachable', }, unconfigured_modules: { because: 'reads config-row counts from celilo DB — always reachable', }, services_credentials: { because: 'reads credential envelopes from celilo DB and decrypts with the local master key; a failed decrypt is itself a measurement (blocked), never silence', }, secrets_decryptable: { because: 'walks encrypted rows in celilo DB and decrypts; a failed decrypt is itself a measurement (blocked), never silence', }, services_reachable: { because: 'the probe outcome IS the measurement: an unreachable service is reachable=false and a finding, so every probe measures its subject', }, machines_reachable: { because: 'the SSH probe outcome IS the measurement: an unreachable machine is reachable=false and a finding, so every probe measures its subject', }, public_dns: { probe: () => auditPublicDns({ records: [{ fqdn: 'git.celilo.computer', companion: false, lastAssertedAt: new Date(0) }], probe: { resolver: '1.1.1.1', echoService: 'https://echo.example', observeIngress: async () => ({ kind: 'undetermined', reason: 'ECONNREFUSED' }), resolve: async () => { throw new Error('not reached: the vantage is down'); }, }, undeterminedThreshold: 1, now: new Date(0), }), because: 'probed: the off-fleet echo vantage is external and can be down; unmeasured', }, disk_space: { probe: () => auditDiskSpace({ results: [ { hostname: 'box', ipAddress: '10.0.0.1', usedPercent: null, message: 'df failed' }, ], }), because: 'probed: a df that could not run measured nothing about the disk', }, transport_reads: { because: 'reads the notification poller recorded read outcomes from celilo DB — always reachable; not happening is itself the drift finding', }, trusted_sources: { probe: () => auditTrustedSources({ firewalls: [], unreachableFirewalls: ['10.0.0.254'] }), because: 'probed: a firewall whose live ruleset cannot be read is unknown, not clean', }, interface_classification: { probe: () => auditInterfaceClassification({ views: [], unreachableFirewalls: ['10.0.0.254'] }), because: 'probed: a firewall whose interfaces cannot be read is unknown, not clean', }, module_integrity: { probe: () => auditModuleIntegrity({ results: [ { success: false, moduleId: 'm', violations: [], error: 'No integrity data found.' }, ], }), because: 'probed: no baseline row, or a stale one, means nothing was verified', }, detect_without_converge: { because: 'static analysis of module manifests and system-config rows in celilo DB — always reachable', }, jail_exemptions: { because: 'reads recorded per-module jail exemptions from celilo DB — the subject is celilo itself and is always reachable; no exemptions is the normal state, not silence', }, }; const ALLOW_LISTED = Object.entries(GATE).filter(([, e]) => !e.probe); describe('recurrence gate: every DriftCategory measures its subject or says why it cannot fail to', () => { test('the gate covers every category in the union (compile-time) and matches the TUI list (run-time)', async () => { // `GATE` is `Record`, so the compiler rejects a // new category until it gets an entry. This run-time line additionally // catches the reverse: an entry for a category no longer in the union. const gateCategories = Object.keys(GATE) as DriftCategory[]; expect(gateCategories.sort()).toEqual([...ALL_CATEGORIES].sort()); }); for (const [category, entry] of Object.entries(GATE) as [DriftCategory, GateEntry][]) { if (!entry.probe) { test(`${category}: allow-listed — ${entry.because}`, () => { expect(entry.because).toBeTruthy(); }); continue; } test(`${category}: cannot reach its subject and still reports unmeasured`, async () => { const probe = entry.probe; if (!probe) expect.unreachable(); const findings = await probe(); const severities: DriftSeverity[] = findings.map((f) => f.severity); expect(severities).toContain('unmeasured'); // The gate exists because silence renders as READY. If an unmeasured // path ever degrades back to drift, the verdict distinction D7 added // is gone and this is the line that says so. const unmeasured = findings.find((f) => f.severity === 'unmeasured') as DriftFinding; expect(unmeasured.category).toBe(category); }); } test('the allow-list is not a dumping ground: 14 categories, each individually justified', () => { // Not a magic number to defend — a tripwire. If this count changes, the // diff is the review: someone moved a category in or out of the set of // "structurally always reaches its subject", and the reason strings // above are where they have to argue it. expect(ALLOW_LISTED).toHaveLength(14); }); });