/** * The six graph shapes the plan names, five of which the live fleet produced. * * They are here rather than in an e2e suite because every one of them is a * property of the walk, and a walk is testable without a fleet. */ import { describe, expect, test } from 'bun:test'; import type { ModuleManifest } from '../manifest/schema'; import type { ProviderRow, ProviderState } from '../services/consumer-cleanup'; import { DEFAULT_CLOSURE_DEPTH, UNBOUNDED_CLOSURE_DEPTH, computeClosure } from './closure'; /** Only the two capability lists matter here; the rest of a manifest does not. */ function manifest(spec: { requires?: string[]; optional?: string[] }): ModuleManifest { return { requires: { capabilities: (spec.requires ?? []).map((name) => ({ name })) }, optional: { capabilities: (spec.optional ?? []).map((name) => ({ name })) }, } as unknown as ModuleManifest; } function deployed(...moduleIds: string[]): ProviderState[] { return moduleIds.map((moduleId) => ({ moduleId, state: 'VERIFIED' })); } describe('computeClosure', () => { test('follows an optional-only edge, and marks it optional', () => { // The live case: a module reaches the internal resolver ONLY optionally. // Following `requires` alone drops the resolver while looking complete. const rows: ProviderRow[] = [{ moduleId: 'technitium', capabilityName: 'dns_internal' }]; const result = computeClosure({ rootModuleId: 'wireguard-manager', manifests: new Map([['wireguard-manager', manifest({ optional: ['dns_internal'] })]]), providerRows: rows, providerStates: deployed('technitium'), }); expect(result.nodes).toHaveLength(1); expect(result.nodes[0]?.moduleId).toBe('technitium'); expect(result.nodes[0]?.optional).toBe(true); }); test('a module reached both ways is not optional', () => { const rows: ProviderRow[] = [ { moduleId: 'caddy', capabilityName: 'private_web' }, { moduleId: 'caddy', capabilityName: 'public_web' }, ]; const result = computeClosure({ rootModuleId: 'app', manifests: new Map([ ['app', manifest({ requires: ['private_web'], optional: ['public_web'] })], ]), providerRows: rows, providerStates: deployed('caddy'), }); expect(result.nodes[0]?.optional).toBe(false); expect(result.nodes[0]?.via).toEqual(['private_web', 'public_web']); }); test('the default depth excludes a three-hop dependency', () => { const rows: ProviderRow[] = [ { moduleId: 'b', capabilityName: 'cap_b' }, { moduleId: 'c', capabilityName: 'cap_c' }, { moduleId: 'd', capabilityName: 'cap_d' }, ]; const manifests = new Map([ ['a', manifest({ requires: ['cap_b'] })], ['b', manifest({ requires: ['cap_c'] })], ['c', manifest({ requires: ['cap_d'] })], ['d', manifest({})], ]); const result = computeClosure({ rootModuleId: 'a', manifests, providerRows: rows, providerStates: deployed('b', 'c', 'd'), }); expect(result.depth).toBe(DEFAULT_CLOSURE_DEPTH); expect(result.nodes.map((n) => n.moduleId)).toEqual(['b', 'c']); expect(result.truncated).toBe(true); }); test('an unbounded walk terminates on a cycle, listing each module once', () => { // caddy requires authentik, authentik requires caddy. The real graph. const rows: ProviderRow[] = [ { moduleId: 'authentik', capabilityName: 'idp' }, { moduleId: 'caddy', capabilityName: 'private_web' }, ]; const manifests = new Map([ ['caddy', manifest({ requires: ['idp'] })], ['authentik', manifest({ requires: ['private_web'] })], ]); const result = computeClosure({ rootModuleId: 'caddy', manifests, providerRows: rows, providerStates: deployed('caddy', 'authentik'), depth: UNBOUNDED_CLOSURE_DEPTH, }); expect(result.nodes.map((n) => n.moduleId)).toEqual(['authentik']); expect(result.truncated).toBe(false); }); test('a capability with several providers highlights every one', () => { // firewall is deliberately multi-provider: an edge provider plus inner layers. const rows: ProviderRow[] = [ { moduleId: 'iptables', capabilityName: 'firewall' }, { moduleId: 'opnsense', capabilityName: 'firewall' }, ]; const result = computeClosure({ rootModuleId: 'app', manifests: new Map([['app', manifest({ requires: ['firewall'] })]]), providerRows: rows, providerStates: deployed('iptables', 'opnsense'), }); expect(result.nodes.map((n) => n.moduleId)).toEqual(['iptables', 'opnsense']); }); test('a module that depends on nothing returns an empty closure, not a failure', () => { const result = computeClosure({ rootModuleId: 'standalone', manifests: new Map([['standalone', manifest({})]]), providerRows: [], providerStates: [], }); expect(result.nodes).toEqual([]); expect(result.truncated).toBe(false); }); test('a module that provides what it consumes is not its own dependency', () => { const rows: ProviderRow[] = [ { moduleId: 'caddy', capabilityName: 'private_web' }, { moduleId: 'other', capabilityName: 'private_web' }, ]; const result = computeClosure({ rootModuleId: 'caddy', manifests: new Map([['caddy', manifest({ requires: ['private_web'] })]]), providerRows: rows, providerStates: deployed('caddy', 'other'), }); expect(result.nodes.map((n) => n.moduleId)).toEqual(['other']); }); test('the shortest hop wins when a module is reachable two ways', () => { const rows: ProviderRow[] = [ { moduleId: 'b', capabilityName: 'cap_b' }, { moduleId: 'shared', capabilityName: 'cap_shared' }, { moduleId: 'shared', capabilityName: 'cap_deep' }, ]; const manifests = new Map([ ['a', manifest({ requires: ['cap_b', 'cap_shared'] })], ['b', manifest({ requires: ['cap_deep'] })], ['shared', manifest({})], ]); const result = computeClosure({ rootModuleId: 'a', manifests, providerRows: rows, providerStates: deployed('b', 'shared'), }); const shared = result.nodes.find((n) => n.moduleId === 'shared'); expect(shared?.hop).toBe(1); }); }); describe('the walk follows real bindings, not declarations', () => { const rows: ProviderRow[] = [ { moduleId: 'cpanel', capabilityName: 'external_web' }, { moduleId: 'caddy', capabilityName: 'public_web' }, { moduleId: 'forgejo', capabilityName: 'source_forge' }, ]; const manifests = new Map([ ['site', manifest({ optional: ['external_web', 'public_web', 'source_forge'] })], ['cpanel', manifest({})], ['caddy', manifest({})], ['forgejo', manifest({})], ]); test('a declared capability the module never called into is NOT a dependency', () => { // The tango-nexus case, which is what celilo#1072 was filed for: four // optional declarations, one actual binding. Following the declarations // claims the module is standing on three things it has never touched. const result = computeClosure({ rootModuleId: 'site', manifests, providerRows: rows, providerStates: deployed('cpanel', 'caddy', 'forgejo'), bindings: new Map([['site', new Map([['external_web', 'cpanel']])]]), }); expect(result.nodes.map((n) => n.moduleId)).toEqual(['cpanel']); }); test('without bindings the walk falls back to declarations, and says more', () => { // Not a silent fallback: the caller decides. A walk with no binding data // answers "what could this reach", which is a different and still useful // question, and it must not be mistaken for the other one. const result = computeClosure({ rootModuleId: 'site', manifests, providerRows: rows, providerStates: deployed('cpanel', 'caddy', 'forgejo'), }); expect(result.nodes.map((n) => n.moduleId).sort()).toEqual(['caddy', 'cpanel', 'forgejo']); }); test('a binding to a DIFFERENT provider of the same capability is not followed', () => { // firewall has several live providers. A consumer bound to one of them is // not thereby standing on the others. const multi: ProviderRow[] = [ { moduleId: 'iptables', capabilityName: 'firewall' }, { moduleId: 'axon', capabilityName: 'firewall' }, ]; const result = computeClosure({ rootModuleId: 'app', manifests: new Map([ ['app', manifest({ requires: ['firewall'] })], ['iptables', manifest({})], ['axon', manifest({})], ]), providerRows: multi, providerStates: deployed('iptables', 'axon'), bindings: new Map([['app', new Map([['firewall', 'iptables']])]]), }); expect(result.nodes.map((n) => n.moduleId)).toEqual(['iptables']); }); test('a delegated provider is reached, though no manifest names it', () => { // The case the whole chain exists for. `app` is BOUND to iptables, so the // manifest walk stops there and the ISP router the packet actually leaves // through never appears — even though iptables cannot reach the internet // without it. celilo wires that edge at hook time; nothing declares it. const multi: ProviderRow[] = [ { moduleId: 'iptables', capabilityName: 'firewall' }, { moduleId: 'axon', capabilityName: 'firewall' }, ]; const result = computeClosure({ rootModuleId: 'app', manifests: new Map([ ['app', manifest({ requires: ['firewall'] })], ['iptables', manifest({})], ['axon', manifest({})], ]), providerRows: multi, providerStates: deployed('iptables', 'axon'), bindings: new Map([['app', new Map([['firewall', 'iptables']])]]), chains: [{ capability: 'firewall', moduleIds: ['iptables', 'axon'] }], }); expect(result.nodes.map((n) => n.moduleId)).toEqual(['iptables', 'axon']); // Hop 2, because it is reached THROUGH iptables. Not a second hop-1 // dependency of the selection, which is what a fan would say. expect(result.nodes.find((n) => n.moduleId === 'axon')?.hop).toBe(2); // A packet has no alternative route to the internet. expect(result.nodes.find((n) => n.moduleId === 'axon')?.optional).toBe(false); expect(result.chains).toEqual([{ capability: 'firewall', moduleIds: ['iptables', 'axon'] }]); }); test('a chain the walk never touched is not reported', () => { // The chain is a fleet fact and the closure is about one module. A selection // that consumes no firewall stands on none of it. const result = computeClosure({ rootModuleId: 'site', manifests, providerRows: rows, providerStates: deployed('cpanel', 'caddy', 'forgejo'), chains: [{ capability: 'firewall', moduleIds: ['iptables', 'axon'] }], }); expect(result.chains).toEqual([]); }); test('the depth bound cuts a delegated provider like any other', () => { // Three layers, bounded at two, so the LAST link is the one the bound cuts. // The delegation edges are walked inside the same loop as the declared ones // and get the same bound; a rule applied after the walk would smuggle the // whole chain into a two-hop answer. // // A two-layer chain would not test this. `iptables` is the only declared // dependency, so the walk exhausts at hop 2 whether or not the delegation // is followed, and the assertion would hold for the wrong reason. const result = computeClosure({ rootModuleId: 'app', manifests: new Map([ ['app', manifest({ requires: ['firewall'] })], ['iptables', manifest({})], ['mid', manifest({})], ['axon', manifest({})], ]), providerRows: [{ moduleId: 'iptables', capabilityName: 'firewall' }], providerStates: deployed('iptables', 'mid', 'axon'), depth: 2, chains: [{ capability: 'firewall', moduleIds: ['iptables', 'mid', 'axon'] }], }); expect(result.nodes.map((n) => n.moduleId)).toEqual(['iptables', 'mid']); expect(result.truncated).toBe(true); // Still reported WHOLE, including the member past the bound. Half a path is // not a path, and the operator asked to see less, not to be told the packet // stops at `mid`. expect(result.chains[0]?.moduleIds).toEqual(['iptables', 'mid', 'axon']); }); test('a module with no bindings at all depends on nothing', () => { const result = computeClosure({ rootModuleId: 'site', manifests, providerRows: rows, providerStates: deployed('cpanel', 'caddy', 'forgejo'), bindings: new Map([['site', new Map()]]), }); expect(result.nodes).toEqual([]); }); });