/** * The wiring, end to end: does `celilo console get` actually carry the chain? * * `orderFirewallChain` is unit-tested, `computeClosure` is unit-tested, and * `loadClosureInputs` is unit-tested against real rows. None of that says the * command joins them up. A dropped argument at this one call site returns a * perfectly well-formed answer with an empty `chains` — which reads as a fleet * with no delegation, and is the failure mode CLAUDE.md names. * * So this drives the REAL command against a REAL database and asserts on what * it emits. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { resetTestDbPath } from '../../test-utils/db-path'; let testDir: string; beforeEach(() => { testDir = mkdtempSync(join(tmpdir(), 'celilo-console-chain-')); process.env.CELILO_DB_PATH = join(testDir, 'test.db'); process.env.CELILO_DATA_DIR = testDir; }); afterEach(() => { rmSync(testDir, { recursive: true, force: true }); resetTestDbPath(); }); /** The live fleet's firewall shape: caddy on iptables, iptables on the router. */ async function seedFleet() { const { getDb } = await import('../../db/client'); const db = getDb(); const consumer = JSON.stringify({ requires: { capabilities: [{ name: 'firewall' }] } }); db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data, state) VALUES ('caddy', 'Caddy', '1.0.0', '/p', '${consumer}', 'VERIFIED')`, ); for (const id of ['iptables', 'axon']) { db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data, state) VALUES ('${id}', '${id}', '1.0.0', '/p', '{}', 'VERIFIED')`, ); } db.$client.run( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES ('iptables', 'firewall', '1.0.0', '{"nat_ip":"192.168.0.253"}')`, ); db.$client.run( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES ('axon', 'firewall', '1.0.0', '{"has_external":true}')`, ); } describe('celilo console get, and the delegation chain', () => { test('--json carries the chain, in order', async () => { await seedFleet(); const { handleConsoleGet } = await import('./console'); const result = handleConsoleGet(['caddy'], { json: true }); expect(result.success).toBe(true); const payload = JSON.parse(result.success ? result.message : '{}'); expect(payload.chains).toEqual([{ capability: 'firewall', moduleIds: ['iptables', 'axon'] }]); }); test('the router is in the closure, though no manifest mentions it', async () => { // caddy requires `firewall` and nothing else. Walking manifests alone stops // at the two registered providers and has no way to know one stands on the // other — which is the whole reason the chain is derived. await seedFleet(); const { handleConsoleGet } = await import('./console'); const payload = JSON.parse( (() => { const r = handleConsoleGet(['caddy'], { json: true }); return r.success ? r.message : '{}'; })(), ); const axon = payload.nodes.find((n: { moduleId: string }) => n.moduleId === 'axon'); expect(axon).toBeDefined(); }); test('the human output prints it as a path, not as more rows', async () => { // A list of two firewalls is exactly the claim being corrected. The arrow is // the information. await seedFleet(); const { handleConsoleGet } = await import('./console'); const result = handleConsoleGet(['caddy'], {}); expect(result.success).toBe(true); expect(result.success ? result.message : '').toContain('iptables -> axon'); }); });