import { describe, expect, test } from 'bun:test'; import type { ModuleManifest } from '../../manifest/schema'; import { type InstalledCapabilityModule, auditCapabilityAbi } from './capability-abi'; function makeModule( id: string, opts: { provides?: Array<{ name: string; version: string }>; requires?: Array<{ name: string; version: string }>; optional?: Array<{ name: string; version: string }>; } = {}, ): InstalledCapabilityModule { const manifest = { id, name: id, version: '1.0.0', celilo_contract: '1.0', provides: opts.provides ? { capabilities: opts.provides.map((p) => ({ ...p, data: {}, functions: [] })), } : { capabilities: [] }, requires: opts.requires ? { capabilities: opts.requires } : { capabilities: [] }, optional: opts.optional ? { capabilities: opts.optional } : undefined, } as unknown as ModuleManifest; // Default INSTALLED so existing assertions keep their `blocked` // semantics. Tests that exercise the IMPORTED-demotion path // override the state explicitly. return { id, state: 'INSTALLED', manifest }; } const RUNTIME = { public_web: '2.0.0', idp: '1.0.0', dns_registrar: '3.0.0', firewall: '1.0.0', dhcp_server: '1.0.0', dns_internal: '1.0.0', }; describe('auditCapabilityAbi', () => { test('no findings when every provider claim matches runtime exactly', async () => { const result = await auditCapabilityAbi({ modules: [ makeModule('caddy', { provides: [{ name: 'public_web', version: '2.0.0' }] }), makeModule('namecheap', { provides: [{ name: 'dns_registrar', version: '3.0.0' }] }), ], contractVersions: RUNTIME, }); expect(result).toEqual([]); }); test('blocked when a provider claims a major-version older than runtime', async () => { const result = await auditCapabilityAbi({ modules: [makeModule('caddy', { provides: [{ name: 'public_web', version: '1.0.0' }] })], contractVersions: RUNTIME, }); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ category: 'capability_abi', severity: 'blocked', code: 'capability_abi_provider_mismatch', subject: 'caddy', actionable: false, }); expect(result[0].details).toContain('cd modules/caddy'); expect(result[0].details).toContain('Application Binary Interface'); }); test('blocked when a provider claims a major-version newer than runtime', async () => { const result = await auditCapabilityAbi({ modules: [makeModule('caddy', { provides: [{ name: 'public_web', version: '3.0.0' }] })], contractVersions: RUNTIME, }); expect(result).toHaveLength(1); expect(result[0].actionable).toBe(false); expect(result[0].details).toContain('Upgrade celilo'); }); test('skips capabilities not in the runtime registry (third-party)', async () => { const result = await auditCapabilityAbi({ modules: [ makeModule('odd-module', { provides: [{ name: 'custom_metric', version: '99.0.0' }] }), ], contractVersions: RUNTIME, }); expect(result).toEqual([]); }); test('blocked when consumer requires a major-version mismatch with the deployed provider', async () => { const result = await auditCapabilityAbi({ modules: [ makeModule('namecheap', { provides: [{ name: 'dns_registrar', version: '3.0.0' }] }), makeModule('caddy', { requires: [{ name: 'dns_registrar', version: '2.0.0' }] }), ], contractVersions: RUNTIME, }); const caddyFinding = result.find( (f) => f.subject === 'caddy' && f.code === 'capability_abi_consumer_mismatch', ); expect(caddyFinding).toBeDefined(); expect(caddyFinding?.message).toContain('caddy requires dns_registrar@2.0.0'); expect(caddyFinding?.message).toContain('namecheap provides 3.0.0'); }); test('blocked when consumer requires a newer minor than provider has', async () => { const result = await auditCapabilityAbi({ modules: [ makeModule('namecheap', { provides: [{ name: 'dns_registrar', version: '3.0.0' }] }), makeModule('lunacycle', { requires: [{ name: 'dns_registrar', version: '3.5.0' }] }), ], contractVersions: { ...RUNTIME, dns_registrar: '3.5.0' }, }); const finding = result.find((f) => f.subject === 'lunacycle'); expect(finding).toBeDefined(); expect(finding?.actionable).toBe(false); expect(finding?.details).toContain('Upgrade namecheap'); }); test('no consumer finding when no provider is installed (deploy preflight handles it)', async () => { const result = await auditCapabilityAbi({ modules: [makeModule('lunacycle', { requires: [{ name: 'idp', version: '1.0.0' }] })], contractVersions: RUNTIME, }); expect(result.find((f) => f.code === 'capability_abi_consumer_mismatch')).toBeUndefined(); }); test('compatible: consumer requires older minor than provider has (additive)', async () => { const result = await auditCapabilityAbi({ modules: [ makeModule('namecheap', { provides: [{ name: 'dns_registrar', version: '3.5.0' }] }), makeModule('caddy', { requires: [{ name: 'dns_registrar', version: '3.0.0' }] }), ], contractVersions: { ...RUNTIME, dns_registrar: '3.5.0' }, }); expect(result).toEqual([]); }); test('also checks optional capabilities (they cause findings on mismatch)', async () => { const result = await auditCapabilityAbi({ modules: [ makeModule('namecheap', { provides: [{ name: 'dns_registrar', version: '3.0.0' }] }), makeModule('caddy', { optional: [{ name: 'dns_registrar', version: '2.0.0' }] }), ], contractVersions: RUNTIME, }); expect( result.some((f) => f.subject === 'caddy' && f.code === 'capability_abi_consumer_mismatch'), ).toBe(true); }); });