/** * Unit tests for `loadHookConfigMap`. * * The helper centralises the "build the config map a hook script will * see" logic across `capability-loader`, `run-named-hook`, and * `health-runner`. The behaviour under test: * * 1. `module_configs` rows are loaded as-is, parsed via `valueJson` * when present. * 2. If `target_ip` is already in the rows, the machine fallback is * skipped entirely (deploy already wrote it). * 3. If `target_ip` isn't set AND the module has a `module_infrastructure` * row pointing at a machine, fill BOTH `target_ip` and `ip.primary` * from `machines.ipAddress`. * 4. If neither is true (no infra row, or infra has no machineId, * or the machine record is gone), neither key gets filled. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { eq } from 'drizzle-orm'; import type { DbClient } from '../db/client'; import { machines, moduleConfigs, moduleInfrastructure, modules, systemConfig } from '../db/schema'; import { upsertModuleConfig } from '../services/module-config'; import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database'; import { loadHookConfigMap } from './load-hook-config'; describe('loadHookConfigMap', () => { let db: DbClient; beforeEach(async () => { db = await setupTestDatabase(); db.insert(modules) .values({ id: 'mod', name: 'test', version: '1.0.0', sourcePath: '/tmp/x', manifestData: {}, }) .run(); }); afterEach(async () => { await cleanupTestDatabase(db); }); test('returns empty config map when no rows + no infrastructure', async () => { const result = await loadHookConfigMap('mod', db); expect(result).toEqual({}); }); test('returns scalar string config values from module_configs', async () => { upsertModuleConfig(db, 'mod', 'hostname', 'caddy'); upsertModuleConfig(db, 'mod', 'acme_email', 'admin@example.com'); const result = await loadHookConfigMap('mod', db); expect(result).toEqual({ hostname: 'caddy', acme_email: 'admin@example.com' }); }); test('parses valueJson for complex types (arrays, objects)', async () => { upsertModuleConfig(db, 'mod', 'hostnames', ['www.example.com', 'example.com']); upsertModuleConfig(db, 'mod', 'plain', 'string-val'); const result = await loadHookConfigMap('mod', db); expect(result.hostnames).toEqual(['www.example.com', 'example.com']); expect(result.plain).toBe('string-val'); }); test('preserves primitive types (numbers, booleans) — Defect 1 round-trip', async () => { // The headline defect that motivated Commit C: primitives used to // stringify on write and come back as strings (e.g. ssh_external_port: // 2222 → stored "2222" → read "2222" → silently broke // firewall.exposeService that expected a number). With upsertModuleConfig // routing through JSON, types round-trip cleanly. upsertModuleConfig(db, 'mod', 'port', 2222); upsertModuleConfig(db, 'mod', 'enabled', true); upsertModuleConfig(db, 'mod', 'disabled', false); const result = await loadHookConfigMap('mod', db); expect(result.port).toBe(2222); expect(typeof result.port).toBe('number'); expect(result.enabled).toBe(true); expect(typeof result.enabled).toBe('boolean'); expect(result.disabled).toBe(false); }); test('preserves an explicit target_ip in module_configs (skips machine fallback)', async () => { upsertModuleConfig(db, 'mod', 'target_ip', '10.0.10.10'); // A machine record exists but should NOT override the explicit value. db.insert(machines) .values({ id: 'machine-1', hostname: 'm1', zone: 'dmz', ipAddress: '192.168.0.99', sshUser: 'root', sshKeyEncrypted: 'x', hardware: { cpu_cores: 1, memory_mb: 512, disk_gb: 10 }, }) .run(); db.insert(moduleInfrastructure) .values({ id: `infra-${Math.random().toString(36).slice(2)}`, moduleId: 'mod', infrastructureType: 'machine', machineId: 'machine-1', }) .run(); const result = await loadHookConfigMap('mod', db); expect(result.target_ip).toBe('10.0.10.10'); // ip.primary is only filled by the fallback path; explicit // target_ip means the helper short-circuits and never looks at the // machine. expect(result['ip.primary']).toBeUndefined(); }); test('fills BOTH target_ip and ip.primary from machine.ipAddress when target_ip is unset', async () => { db.insert(machines) .values({ id: 'machine-1', hostname: 'caddy-host', zone: 'dmz', ipAddress: '10.0.10.10', sshUser: 'root', sshKeyEncrypted: 'x', hardware: { cpu_cores: 1, memory_mb: 512, disk_gb: 10 }, }) .run(); db.insert(moduleInfrastructure) .values({ id: `infra-${Math.random().toString(36).slice(2)}`, moduleId: 'mod', infrastructureType: 'machine', machineId: 'machine-1', }) .run(); const result = await loadHookConfigMap('mod', db); expect(result.target_ip).toBe('10.0.10.10'); expect(result['ip.primary']).toBe('10.0.10.10'); }); test('leaves target_ip unset when there is no infrastructure record', async () => { upsertModuleConfig(db, 'mod', 'hostname', 'caddy'); // no moduleInfrastructure row inserted const result = await loadHookConfigMap('mod', db); expect(result.hostname).toBe('caddy'); expect(result.target_ip).toBeUndefined(); expect(result['ip.primary']).toBeUndefined(); }); test('leaves target_ip unset when infrastructure row has no machineId', async () => { db.insert(moduleInfrastructure) .values({ id: `infra-${Math.random().toString(36).slice(2)}`, moduleId: 'mod', infrastructureType: 'container_service', machineId: null, }) .run(); const result = await loadHookConfigMap('mod', db); expect(result.target_ip).toBeUndefined(); expect(result['ip.primary']).toBeUndefined(); }); }); /** * The 2026-08-14 outage, as a test. * * `technitium` declares `vpn_subnet` with `source: system` and * `derive_from: $system:network.control-plane-vpn.subnet`. The system key was * set on the fleet AFTER the module had already been configured, so no * `module_configs` row was ever written for it — `applyDeclarativeDerivations` * skips a `source: system` variable once the module has any stored config for * it, and nothing re-derives one that is absent. The variable is * `required: false`, so deploy-validation passed without comment. * * The consumer was a capability factory, which gets its config from * `loadHookConfigMap`. A direct `module_configs` select cannot see a value that * was never stored, so the factory built split-horizon DNS with no view for the * admin VPN: queries from it matched nothing, returned NOERROR with zero * records, fell through to public DNS, and the operator could not reach the * forge while every service reported healthy. * * The fix is that the hook config map is built from the resolution context — * which recomputes derived values on every build — not from the table alone. */ describe('loadHookConfigMap: derived values the config table never stored', () => { let db: DbClient; const VPN_SUBNET_KEY = 'network.control-plane-vpn.subnet'; const VPN_SUBNET = '10.255.255.0/24'; beforeEach(async () => { db = await setupTestDatabase(); db.insert(modules) .values({ id: 'technitium', name: 'Technitium DNS', version: '1.0.0', sourcePath: '/tmp/technitium', manifestData: { variables: { owns: [ { name: 'hostname', type: 'string', source: 'user' }, { name: 'vpn_subnet', type: 'string', source: 'system', required: false, derive_from: `$system:${VPN_SUBNET_KEY}`, }, ], }, }, }) .run(); // The module was configured first: it has its user rows, and no row for // the derived variable. upsertModuleConfig(db, 'technitium', 'hostname', 'dns-int'); }); afterEach(async () => { await cleanupTestDatabase(db); }); test('a hook sees a source:system derive whose system key was set after the module was configured', async () => { db.insert(systemConfig).values({ key: VPN_SUBNET_KEY, value: VPN_SUBNET }).run(); const result = await loadHookConfigMap('technitium', db); expect(result.hostname).toBe('dns-int'); expect(result.vpn_subnet).toBe(VPN_SUBNET); }); test('recomputing does not write the derived value back into module_configs', async () => { db.insert(systemConfig).values({ key: VPN_SUBNET_KEY, value: VPN_SUBNET }).run(); await loadHookConfigMap('technitium', db); // Reading a hook's config is not a deploy. It must not seed or refresh // stored config as a side effect — that is how a stale snapshot gets // written in the first place. const stored = db .select() .from(moduleConfigs) .where(eq(moduleConfigs.moduleId, 'technitium')) .all(); expect(stored.map((row) => row.key).sort()).toEqual(['hostname']); }); test('an optional derive whose system key is still unset stays absent, not an error', async () => { // No systemConfig row at all — the state the fleet was in before the VPN // subnet was declared. `required: false`, so this is silence, not failure. const result = await loadHookConfigMap('technitium', db); expect(result.hostname).toBe('dns-int'); expect(result.vpn_subnet).toBeUndefined(); }); test('an already-stored value reaches the hook unchanged', async () => { // Precedence: a stored row wins, and the recomputed context only fills // keys the table has no row for. Every derived value on the fleet today is // stored, so this is what keeps the new read path from changing what any // deployed hook already receives — the change is purely additive here. // (Once derived values stop being persisted, the stored row disappears and // the recomputed value is all that is left.) db.insert(systemConfig).values({ key: VPN_SUBNET_KEY, value: VPN_SUBNET }).run(); upsertModuleConfig(db, 'technitium', 'vpn_subnet', '10.99.0.0/24'); const result = await loadHookConfigMap('technitium', db); expect(result.vpn_subnet).toBe('10.99.0.0/24'); }); }); /** * Recomputation must not make a hook's config LESS available than reading the * table did. * * `applyDeclarativeDerivations` throws when a `required: true` variable's * derivation fails, which at generate time is exactly right — a deploy that * cannot resolve a required value should stop. But this reader also serves * health checks and `module run-hook`, which used to be unable to fail this * way at all: they read stored rows, and a stored row cannot throw. A provider * that is paused, removed, or not yet deployed would take its consumers' * health checks down with it — reporting the consumer as broken when the * consumer is fine. */ describe('loadHookConfigMap: a failing derive does not take the hook down', () => { let db: DbClient; beforeEach(async () => { db = await setupTestDatabase(); db.insert(modules) .values({ id: 'caddy', name: 'Caddy', version: '1.0.0', sourcePath: '/tmp/caddy', manifestData: { variables: { owns: [ { name: 'hostname', type: 'string', source: 'user' }, { name: 'primary_domain', type: 'string', source: 'capability', required: true, derive_from: '$capability:dns_registrar.zone.primary_domain', }, ], }, }, }) .run(); upsertModuleConfig(db, 'caddy', 'hostname', 'caddy'); }); afterEach(async () => { await cleanupTestDatabase(db); }); test('stored config still reaches the hook when a required derive cannot resolve', async () => { // No `dns_registrar` capability is registered — the provider is paused, // removed, or has not been deployed yet. const result = await loadHookConfigMap('caddy', db); expect(result.hostname).toBe('caddy'); expect(result.primary_domain).toBeUndefined(); }); });