import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { existsSync } from 'node:fs'; import { rm } from 'node:fs/promises'; import { and, eq } from 'drizzle-orm'; import { type DbClient, createDbClient } from '../db/client'; import { containerServices, machines, moduleConfigs, moduleInfrastructure, moduleSystems, modules, } from '../db/schema'; import type { ModuleManifest } from '../manifest/schema'; import { backfillModuleSystems, getModuleSystems, recordDeployedSystemForModule, upsertDeployedSystem, } from './deployed-systems'; const TEST_DB_PATH = './test-deployed-systems.db'; /** * Coverage for the one-time `module_systems` upgrade backfill * (openspec/specs/module-systems-addressing/spec.md): a deployment created before 0007 has its * host data in module_configs / ip_allocations / module_infrastructure but an * empty module_systems, and the refactored hooks resolve to no system. The * backfill reconstructs it. Mirrors the real turnip prod state (caddy etc.: * proxmox container_service, target_ip with /24, vmid in module_configs). */ const SERVICE_ID = 'svc-proxmox'; function ensureProxmoxService(db: DbClient): void { if ( db .select() .from(containerServices) .all() .some((s) => s.id === SERVICE_ID) ) return; db.insert(containerServices) .values({ id: SERVICE_ID, serviceId: SERVICE_ID, name: 'Proxmox', providerName: 'proxmox', zones: ['dmz', 'app', 'internal'] as Array< 'internal' | 'dmz' | 'app' | 'secure' | 'external' >, apiCredentialsEncrypted: JSON.stringify({ encryptedValue: '', iv: '', authTag: '' }), providerConfig: { default_target_node: 'pve', lxc_template: 't', storage: 's' }, verified: true, }) .run(); } function seedProxmoxModule( db: DbClient, opts: { id: string; hostname: string; zone: string; targetIp: string; vmid: number }, ): void { const serviceId = SERVICE_ID; ensureProxmoxService(db); db.insert(modules) .values({ id: opts.id, name: opts.id, version: '1.0.0', manifestData: { requires: { system: { zone: opts.zone } } }, sourcePath: `/tmp/${opts.id}`, state: 'VERIFIED', }) .run(); db.insert(moduleInfrastructure) .values({ id: `infra-${opts.id}`, moduleId: opts.id, infrastructureType: 'container_service', serviceId, }) .run(); for (const [key, value] of [ ['hostname', opts.hostname], ['zone', opts.zone], ['target_ip', opts.targetIp], ['vmid', String(opts.vmid)], ]) { db.insert(moduleConfigs) .values({ moduleId: opts.id, key, value, valueJson: JSON.stringify(value) }) .run(); } } describe('backfillModuleSystems', () => { let db: DbClient; beforeEach(() => { db = createDbClient({ path: TEST_DB_PATH }); }); afterEach(async () => { db.$client.close(); for (const suffix of ['', '-shm', '-wal']) { const p = `${TEST_DB_PATH}${suffix}`; if (existsSync(p)) await rm(p); } }); test('reconstructs a pre-0007 proxmox deployment into module_systems', () => { seedProxmoxModule(db, { id: 'caddy', hostname: 'www', zone: 'dmz', targetIp: '10.0.10.10/24', vmid: 200, }); expect(getModuleSystems('caddy', db)).toHaveLength(0); const filled = backfillModuleSystems(db); expect(filled).toEqual(['caddy']); const systems = getModuleSystems('caddy', db); expect(systems).toHaveLength(1); expect(systems[0]).toMatchObject({ name: 'main', hostname: 'www', ipv4_address: '10.0.10.10', // CIDR stripped zone: 'dmz', infrastructure: { type: 'container_service', serviceId: 'svc-proxmox', vmid: 200 }, }); }); test('is idempotent — a second run backfills nothing', () => { seedProxmoxModule(db, { id: 'authentik', hostname: 'authentik', zone: 'app', targetIp: '10.0.20.10/24', vmid: 201, }); expect(backfillModuleSystems(db)).toEqual(['authentik']); expect(backfillModuleSystems(db)).toEqual([]); expect(getModuleSystems('authentik', db)).toHaveLength(1); }); test('does not overwrite a system already recorded post-refactor', () => { seedProxmoxModule(db, { id: 'lunacycle', hostname: 'lunacycle', zone: 'app', targetIp: '10.0.20.11/24', vmid: 202, }); // Simulate a deploy-time recording already present with a different IP. db.insert(moduleSystems) .values({ moduleId: 'lunacycle', name: 'main', hostname: 'lunacycle', ipv4Address: '10.0.20.99', zone: 'app', infraType: 'container_service', serviceId: 'svc-proxmox', vmid: 202, }) .run(); expect(backfillModuleSystems(db)).toEqual([]); expect(getModuleSystems('lunacycle', db)[0].ipv4_address).toBe('10.0.20.99'); }); test('backfills a machine-pool deployment from the machine IP', () => { db.insert(machines) .values({ id: 'm-1', hostname: 'pi-dns', ipAddress: '192.168.0.151', sshUser: 'peba', sshKeyEncrypted: JSON.stringify({ encryptedValue: '', iv: '', authTag: '' }), hardware: { cpu_cores: 4, memory_mb: 8192, disk_gb: 64 }, zone: 'internal', }) .run(); db.insert(modules) .values({ id: 'technitium', name: 'technitium', version: '1.0.0', manifestData: { requires: { system: { zone: 'internal' } } }, sourcePath: '/tmp/technitium', state: 'VERIFIED', }) .run(); db.insert(moduleInfrastructure) .values({ id: 'infra-technitium', moduleId: 'technitium', infrastructureType: 'machine', machineId: 'm-1', }) .run(); db.insert(moduleConfigs) .values({ moduleId: 'technitium', key: 'hostname', value: 'dns-int', valueJson: '"dns-int"' }) .run(); expect(backfillModuleSystems(db)).toEqual(['technitium']); const systems = getModuleSystems('technitium', db); expect(systems[0]).toMatchObject({ hostname: 'dns-int', ipv4_address: '192.168.0.151', zone: 'internal', infrastructure: { type: 'machine', machineId: 'm-1' }, }); expect(systems[0].infrastructure.vmid).toBeUndefined(); }); // The control plane is recorded by THIS path, not the deploy path, so the two // must agree on zone precedence. They didn't: the deploy path took the machine's // zone while backfill still took the manifest's, so celilo-mgmt was persisted as // `internal` while sitting on `secure-mgmt` — and the firewall trusted the wrong // subnet, silently dropping celilo's own SSH to the fleet. test('backfill records the MACHINE zone, matching the deploy path', () => { db.insert(machines) .values({ id: 'm-mgmt-bf', hostname: 'celilo-mgr', ipAddress: '10.0.120.100', sshUser: 'root', sshKeyEncrypted: JSON.stringify({ encryptedValue: '', iv: '', authTag: '' }), hardware: { cpu_cores: 4, memory_mb: 8192, disk_gb: 64 }, zone: 'secure-mgmt', }) .run(); db.insert(modules) .values({ id: 'celilo-mgmt', name: 'celilo-mgmt', version: '1.0.0', manifestData: { requires: { system: { zone: 'internal' } } }, sourcePath: '/tmp/celilo-mgmt', state: 'VERIFIED', }) .run(); db.insert(moduleInfrastructure) .values({ id: 'infra-celilo-mgmt', moduleId: 'celilo-mgmt', infrastructureType: 'machine', machineId: 'm-mgmt-bf', }) .run(); db.insert(moduleConfigs) .values({ moduleId: 'celilo-mgmt', key: 'hostname', value: 'celilo-mgr', valueJson: '"celilo-mgr"', }) .run(); expect(backfillModuleSystems(db)).toEqual(['celilo-mgmt']); expect(getModuleSystems('celilo-mgmt', db)[0]).toMatchObject({ zone: 'secure-mgmt', ipv4_address: '10.0.120.100', }); }); test('backfill records a local machine by its interface in the resolved zone', () => { db.insert(machines) .values({ id: 'm-local-bf', hostname: 'celilo-mgr', // Local-execution sentinel, not the machine's network identity. ipAddress: '127.0.0.1', sshUser: 'jem', sshKeyEncrypted: JSON.stringify({ encryptedValue: '', iv: '', authTag: '' }), hardware: { cpu_cores: 4, memory_mb: 8192, disk_gb: 64 }, zone: 'secure-mgmt', interfaces: [ { name: 'en0', ipAddress: '192.168.0.32', zone: 'external' }, { name: 'ens18', ipAddress: '10.77.20.32', zone: 'secure-mgmt' }, ], }) .run(); db.insert(modules) .values({ id: 'celilo-mgmt', name: 'celilo-mgmt', version: '1.0.0', manifestData: { requires: { system: { zone: 'internal' } } }, sourcePath: '/tmp/celilo-mgmt', state: 'VERIFIED', }) .run(); db.insert(moduleInfrastructure) .values({ id: 'infra-celilo-mgmt-local', moduleId: 'celilo-mgmt', infrastructureType: 'machine', machineId: 'm-local-bf', }) .run(); db.insert(moduleConfigs) .values({ moduleId: 'celilo-mgmt', key: 'hostname', value: 'celilo-mgr', valueJson: '"celilo-mgr"', }) .run(); expect(backfillModuleSystems(db)).toEqual(['celilo-mgmt']); expect(getModuleSystems('celilo-mgmt', db)[0]).toMatchObject({ zone: 'secure-mgmt', ipv4_address: '10.77.20.32', }); }); // The control-plane case. celilo-mgmt declares `internal` (it bootstraps before // any firewall exists) but in a segmented fleet the operator earmarks a box on // `secure-mgmt`. What gets RECORDED must be where the system actually is, since // the firewall's trusted source is derived from it — recording the manifest's // zone silently trusts the wrong subnet and drops celilo's own SSH to the fleet. // // This fails two distinct ways before the fix: the machine's zone was never // consulted, AND a hand-maintained zone list omitted `secure-mgmt`, so validating // it returned null and fell back to a wrong-but-valid zone. test('records the zone of the MACHINE it landed on, not the manifest minimum', async () => { db.insert(machines) .values({ id: 'm-mgmt', hostname: 'celilo-mgr', ipAddress: '10.0.120.100', sshUser: 'root', sshKeyEncrypted: JSON.stringify({ encryptedValue: '', iv: '', authTag: '' }), hardware: { cpu_cores: 4, memory_mb: 8192, disk_gb: 64 }, zone: 'secure-mgmt', earmarkedModule: 'celilo-mgmt', }) .run(); db.insert(modules) .values({ id: 'celilo-mgmt', name: 'celilo-mgmt', version: '1.0.0', manifestData: { requires: { system: { zone: 'internal' } } }, sourcePath: '/tmp/celilo-mgmt', state: 'VERIFIED', }) .run(); db.insert(moduleConfigs) .values({ moduleId: 'celilo-mgmt', key: 'hostname', value: 'celilo-mgr', valueJson: '"celilo-mgr"', }) .run(); const recorded = await recordDeployedSystemForModule( 'celilo-mgmt', { requires: { system: { zone: 'internal' } } } as ModuleManifest, { type: 'machine', machineId: 'm-mgmt' }, db, ); expect(recorded[0]).toMatchObject({ zone: 'secure-mgmt', ipv4_address: '10.0.120.100' }); expect(getModuleSystems('celilo-mgmt', db)[0]).toMatchObject({ zone: 'secure-mgmt' }); }); test('records a local machine by its interface in the resolved zone', async () => { db.insert(machines) .values({ id: 'm-local', hostname: 'celilo-mgr', // Local-execution sentinel, not the machine's network identity. ipAddress: '127.0.0.1', sshUser: 'jem', sshKeyEncrypted: JSON.stringify({ encryptedValue: '', iv: '', authTag: '' }), hardware: { cpu_cores: 4, memory_mb: 8192, disk_gb: 64 }, zone: 'secure-mgmt', earmarkedModule: 'celilo-mgmt', interfaces: [ { name: 'en0', ipAddress: '192.168.0.32', zone: 'external' }, { name: 'ens18', ipAddress: '10.77.20.32', zone: 'secure-mgmt' }, ], }) .run(); db.insert(modules) .values({ id: 'celilo-mgmt', name: 'celilo-mgmt', version: '1.0.0', manifestData: { requires: { system: { zone: 'internal' } } }, sourcePath: '/tmp/celilo-mgmt', state: 'VERIFIED', }) .run(); db.insert(moduleConfigs) .values({ moduleId: 'celilo-mgmt', key: 'hostname', value: 'celilo-mgr', valueJson: '"celilo-mgr"', }) .run(); const recorded = await recordDeployedSystemForModule( 'celilo-mgmt', { requires: { system: { zone: 'internal' } } } as ModuleManifest, { type: 'machine', machineId: 'm-local' }, db, ); expect(recorded[0]).toMatchObject({ zone: 'secure-mgmt', ipv4_address: '10.77.20.32', }); }); test('skips an API-only module (no declared systems)', () => { ensureProxmoxService(db); db.insert(modules) .values({ id: 'namecheap', name: 'namecheap', version: '1.0.0', manifestData: { requires: {} }, sourcePath: '/tmp/namecheap', state: 'INSTALLED', }) .run(); db.insert(moduleInfrastructure) .values({ id: 'infra-namecheap', moduleId: 'namecheap', infrastructureType: 'container_service', serviceId: 'svc-proxmox', }) .run(); expect(backfillModuleSystems(db)).toEqual([]); expect(getModuleSystems('namecheap', db)).toHaveLength(0); }); }); /** * Canonical instance sizing (ISS-0150): sizing is seeded onto module_systems * once at first provision and then OWNED by `celilo proxmox … resize` — a routine * re-deploy must never reset a resized instance back to its manifest minimum. */ describe('upsertDeployedSystem sizing — seed-once (ISS-0150)', () => { let db: DbClient; beforeEach(() => { db = createDbClient({ path: TEST_DB_PATH }); db.insert(modules) .values({ id: 'm1', name: 'm1', version: '1.0.0', manifestData: {}, sourcePath: '/tmp/m1', state: 'VERIFIED', }) .run(); }); afterEach(async () => { db.$client.close(); for (const suffix of ['', '-shm', '-wal']) { const p = `${TEST_DB_PATH}${suffix}`; if (existsSync(p)) await rm(p); } }); const sizeArgs = (memory: number) => ({ name: 'main', hostname: 'h', ipv4Address: '10.0.0.5/24', zone: 'app' as const, infraType: 'container_service' as const, vmid: 200, cpu: 4, memory, disk: 80, }); const row = () => db .select() .from(moduleSystems) .where(and(eq(moduleSystems.moduleId, 'm1'), eq(moduleSystems.name, 'main'))) .get(); test('seeds sizing on first insert', () => { upsertDeployedSystem(db, 'm1', sizeArgs(8192)); const r = row(); expect(r?.cpu).toBe(4); expect(r?.memory).toBe(8192); expect(r?.disk).toBe(80); }); test('a re-deploy does NOT reset a resized instance to the manifest minimum', () => { upsertDeployedSystem(db, 'm1', sizeArgs(8192)); // first provision: seed 8 GB // Simulate `celilo proxmox vm resize` bumping the canonical size to 16 GB. db.update(moduleSystems) .set({ memory: 16384 }) .where(and(eq(moduleSystems.moduleId, 'm1'), eq(moduleSystems.name, 'main'))) .run(); // Re-deploy passes the manifest minimum (8 GB) again — must be ignored. upsertDeployedSystem(db, 'm1', sizeArgs(8192)); expect(row()?.memory).toBe(16384); expect(row()?.cpu).toBe(4); }); });