/** * The console projection, against a real schema. * * The payload SHAPE is the point of these reads, so the assertions are about * what is present and what is deliberately absent, not only about values. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import type { DbClient } from '../db/client'; import { NETWORK_ZONES } from '../db/schema'; import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database'; import { consoleStatus, loadClosureInputs } from './projection'; /** A manifest big enough to notice if it ever leaked into the payload. */ const FAT_MANIFEST = JSON.stringify({ id: 'caddy-internal', padding: 'x'.repeat(4096), requires: { capabilities: [{ name: 'idp' }] }, }); describe('consoleStatus', () => { let db: DbClient; beforeEach(async () => { db = await setupTestDatabase(); db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data, state) VALUES ('caddy-internal', 'Caddy', '1.2.0', '/path', '${FAT_MANIFEST}', 'VERIFIED')`, ); db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data, state) VALUES ('api-only', 'API only', '0.1.0', '/path', '{}', 'VERIFIED')`, ); db.$client.run( `INSERT INTO module_systems (module_id, name, hostname, ipv4_address, zone, infra_type) VALUES ('caddy-internal', 'web', 'caddy-int', '10.0.10.14', 'dmz', 'container_service')`, ); }); afterEach(async () => { await cleanupTestDatabase(db); }); test('serves the canonical zone order rather than a copy', () => { // A zone added to NETWORK_ZONES must reach the console without a console // release, so the payload carries the list itself. expect(consoleStatus(db).zones).toEqual([...NETWORK_ZONES]); }); test('does NOT carry manifestData', () => { // The whole reason these reads exist. `module list --json` is 156 KB for 23 // modules because it embeds this blob on every row. const payload = JSON.stringify(consoleStatus(db)); expect(payload).not.toContain('padding'); expect(payload.length).toBeLessThan(2000); }); test('carries a module with no system rather than omitting it', () => { // A closure that names a module the topology then cannot draw is // indistinguishable from a defect, so the payload keeps it with an empty // systems list and the renderer decides where to put it. const found = consoleStatus(db).modules.find((m) => m.id === 'api-only'); expect(found).toBeDefined(); expect(found?.systems).toEqual([]); }); test('carries each system with the address recorded in deployment state', () => { const caddy = consoleStatus(db).modules.find((m) => m.id === 'caddy-internal'); expect(caddy?.systems).toEqual([{ hostname: 'caddy-int', address: '10.0.10.14', zone: 'dmz' }]); }); test('a deployed module with no monitor reads as not observed, not as healthy', () => { const caddy = consoleStatus(db).modules.find((m) => m.id === 'caddy-internal'); expect(caddy?.health.cell).toBe('not observed'); expect(caddy?.health.monitored).toBe(false); }); test('an undeployed module is distinguished from an unobserved one', () => { db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data, state) VALUES ('imported', 'Imported', '0.1.0', '/path', '{}', 'IMPORTED')`, ); const imported = consoleStatus(db).modules.find((m) => m.id === 'imported'); // "not observed" is a finding about a deployed module. Saying it about a // module that was never deployed would be noise. expect(imported?.health.cell).toBe('not deployed'); }); test('reports no backup as null rather than as zero', () => { const caddy = consoleStatus(db).modules.find((m) => m.id === 'caddy-internal'); expect(caddy?.lastBackupAt).toBeNull(); expect(caddy?.lastBackupFailed).toBe(false); }); test('modules come back in a stable order', () => { const first = consoleStatus(db).modules.map((m) => m.id); const second = consoleStatus(db).modules.map((m) => m.id); expect(first).toEqual(second); expect(first).toEqual([...first].sort()); }); }); /** * The three facts the roster's lamps read, and the unit the ages are in. * * Every one of these is a wrong-but-plausible number rather than a crash. A * timestamp in the wrong unit renders as an age that looks like a fresh backup. * A module reported as watched when nothing runs its monitor reads as healthy. * Neither throws, and neither is visible in review. */ describe('consoleStatus derived facts', () => { let db: DbClient; const DAY = 86_400_000; const HOOKED = JSON.stringify({ id: 'forgejo', hooks: { on_backup: { script: './backup.ts' } }, backup: { schedule: 'daily' }, }); function moduleRow(id: string, manifest: string): void { db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data, state) VALUES ('${id}', '${id}', '1.0.0', '/path', '${manifest}', 'VERIFIED')`, ); } function backupRow(id: string, moduleId: string, status: string, completedAtMs: number): void { db.$client.run( `INSERT INTO backup_storages (id, storage_id, name, provider_name, credentials_encrypted, provider_config) VALUES ('st-1', 'aws', 'AWS', 's3', 'x', '{}') ON CONFLICT DO NOTHING`, ); db.$client.run( `INSERT INTO backups (id, module_id, storage_id, storage_path, backup_type, status, started_at, completed_at, metadata) VALUES ('${id}', '${moduleId}', 'st-1', 'p/${id}', 'module_data', '${status}', ${Math.floor(completedAtMs / 1000)}, ${Math.floor(completedAtMs / 1000)}, '{}')`, ); } function monitorRow(target: string, opts: { policy?: boolean; ran?: boolean }): void { db.$client.run( `INSERT INTO monitors (id, kind, target, interval_minutes, enabled, escalation_policy_id, last_run_at) VALUES ('mon-${target}', 'module_hook', '${target}', 60, 1, ${opts.policy ? "'pol-1'" : 'NULL'}, ${opts.ran ? Math.floor(Date.now() / 1000) : 'NULL'})`, ); } function moduleOf(id: string) { const found = consoleStatus(db).modules.find((m) => m.id === id); if (!found) throw new Error(`${id} missing from the projection`); return found; } beforeEach(async () => { db = await setupTestDatabase(); db.$client.run( `INSERT INTO escalation_policies (id, name) VALUES ('pol-1', 'oncall') ON CONFLICT DO NOTHING`, ); }); afterEach(async () => { await cleanupTestDatabase(db); }); test('lastBackupAt is epoch MILLISECONDS', () => { // The whole reason this test exists. It was seconds, the protocol on the // other side says milliseconds, and nothing joined the two yet — so the // factor of a thousand sat there looking like working code. const at = Date.now() - 2 * DAY; moduleRow('forgejo', HOOKED); backupRow('b1', 'forgejo', 'completed', at); const lastBackupAt = moduleOf('forgejo').lastBackupAt; expect(lastBackupAt).not.toBeNull(); // Within a day of now in ms. In seconds this would be ~1.8 billion, which // is fifty-odd years, and would render as an absurd age rather than throw. expect(Date.now() - (lastBackupAt as number)).toBeLessThan(3 * DAY); }); test('a module with no monitor at all is unwatched', () => { moduleRow('forgejo', HOOKED); expect(moduleOf('forgejo').unwatched).toBe(true); }); test('a monitor that has NEVER RUN is not coverage', () => { // It produces exactly as much evidence as no monitor does. moduleRow('forgejo', HOOKED); monitorRow('forgejo', { policy: true, ran: false }); expect(moduleOf('forgejo').unwatched).toBe(true); }); test('a monitor that runs makes the module watched', () => { moduleRow('forgejo', HOOKED); monitorRow('forgejo', { policy: true, ran: true }); expect(moduleOf('forgejo').unwatched).toBe(false); }); test('a running monitor with no escalation policy pages nobody', () => { // Worse than unwatched in one respect: it looks monitored. The alert is // raised on time and reaches no one. moduleRow('forgejo', HOOKED); monitorRow('forgejo', { policy: false, ran: true }); const module = moduleOf('forgejo'); expect(module.unwatched).toBe(false); expect(module.pagesNobody).toBe(true); }); test('an overdue daily backup is stale', () => { moduleRow('forgejo', HOOKED); backupRow('b1', 'forgejo', 'completed', Date.now() - 5 * DAY); expect(moduleOf('forgejo').backupStale).toBe(true); }); test('a fresh daily backup is not', () => { moduleRow('forgejo', HOOKED); backupRow('b1', 'forgejo', 'completed', Date.now() - 60_000); expect(moduleOf('forgejo').backupStale).toBe(false); }); test('a module with NO on_backup hook is never stale', () => { // There is nothing to run. Reporting eighteen such modules as overdue is // how this page becomes an alarm nobody can act on (celilo#1131). moduleRow('caddy', JSON.stringify({ id: 'caddy' })); expect(moduleOf('caddy').backupStale).toBe(false); }); test('a hooked module that has NEVER been backed up is stale', () => { // Distinct from the case above: this one was supposed to run and did not. moduleRow('forgejo', HOOKED); expect(moduleOf('forgejo').backupStale).toBe(true); }); }); /** * The reach probe for the chain (CLAUDE.md, "a check that cannot reach the * thing it is checking"). * * `orderFirewallChain` is unit-tested against hand-built rows. That says * nothing about whether the closure's loader queries the right table with the * right filter, and a wrong query here returns a well-formed empty list that * reads exactly like a fleet with one firewall. So this drives the REAL loader * against real `capabilities` rows. */ describe('loadClosureInputs, and the chain it carries', () => { let db: DbClient; beforeEach(async () => { db = await setupTestDatabase(); for (const id of ['iptables', 'axon', 'caddy']) { db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data, state) VALUES ('${id}', '${id}', '1.0.0', '/path', '{}', 'VERIFIED')`, ); } }); afterEach(async () => { await cleanupTestDatabase(db); }); function firewallProvider(moduleId: string, data: string) { db.$client.run( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES ('${moduleId}', 'firewall', '1.0.0', '${data}')`, ); } test('reads the live registration and orders it, iptables first', () => { firewallProvider('iptables', '{"firewall_ip":"192.168.0.254","nat_ip":"192.168.0.253"}'); firewallProvider('axon', '{"has_external":true,"router_ip":"192.168.0.1"}'); expect(loadClosureInputs(db).chains).toEqual([ { capability: 'firewall', moduleIds: ['iptables', 'axon'] }, ]); }); test('a non-firewall capability is not swept into the chain', () => { // `axon` provides dhcp_server too. A loader that read every row would put a // module in the packet's path on the strength of an unrelated capability. firewallProvider('iptables', '{"firewall_ip":"192.168.0.254","nat_ip":"192.168.0.253"}'); firewallProvider('axon', '{"has_external":true}'); db.$client.run( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES ('caddy', 'public_web', '1.0.0', '{}')`, ); expect(loadClosureInputs(db).chains[0]?.moduleIds).toEqual(['iptables', 'axon']); }); test('a fleet with one firewall has no chain', () => { firewallProvider('axon', '{"has_external":true}'); expect(loadClosureInputs(db).chains).toEqual([]); }); });