import { describe, expect, test } from 'bun:test'; import { getSingularSystemSpec } from './schema'; import { validateCapabilityNames, validateCapabilityRequirements, validateDeriveFromSources, validateHookContract, validateManifest, validateProvidesNoCrossCapabilityRefs, validateSubmoduleDeclaration, validateSubmoduleManifest, validateVariableSources, } from './validate'; const CONTRACT_LINE = 'celilo_contract: "1.0"'; describe('validateManifest', () => { test('should validate minimal valid manifest', () => { const yaml = ` ${CONTRACT_LINE} id: homebridge name: Homebridge version: 1.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.id).toBe('homebridge'); expect(result.data.name).toBe('Homebridge'); expect(result.data.version).toBe('1.0.0'); expect(result.data.requires.capabilities).toEqual([]); expect(result.data.provides.capabilities).toEqual([]); } }); test('should validate complete homebridge manifest', () => { const yaml = ` ${CONTRACT_LINE} id: homebridge name: Homebridge version: 1.0.0 description: HomeKit bridge for smart home devices requires: capabilities: [] system: cpu: 1 memory: 1024 disk: 20 zone: app provides: capabilities: [] variables: owns: - name: container_ip type: string required: true description: IP address for Homebridge container source: user - name: homebridge_pin type: string required: true description: HomeKit PIN code source: user `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.id).toBe('homebridge'); expect(result.data.variables.owns).toHaveLength(2); expect(result.data.requires.system?.cpu).toBe(1); expect(result.data.requires.system?.zone).toBe('app'); } }); test('requires.system.type defaults to lxc when omitted', () => { const yaml = ` ${CONTRACT_LINE} id: homebridge name: Homebridge version: 1.0.0 requires: system: cpu: 1 zone: app `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.requires.system?.type).toBe('lxc'); } }); test('requires.system.type accepts an explicit vm', () => { const yaml = ` ${CONTRACT_LINE} id: forgejo-runner name: Forgejo Runner version: 1.0.0 requires: system: cpu: 4 memory: 8192 type: vm zone: dmz `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.requires.system?.type).toBe('vm'); } }); test('requires.system.type rejects an unknown infra type', () => { const yaml = ` ${CONTRACT_LINE} id: homebridge name: Homebridge version: 1.0.0 requires: system: type: container zone: app `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should validate dns-external manifest with capability provider', () => { const yaml = ` ${CONTRACT_LINE} id: dns-external name: DNS External (Knot) version: 1.0.0 requires: capabilities: [] provides: capabilities: - name: dns_external version: 1.0.0 data: nameserver: "$self:container_ip" zone: "$self:zone" api_endpoint: "http://$self:container_ip:8053" variables: owns: - name: zone type: string required: true source: user - name: container_ip type: string required: true source: user `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.provides.capabilities).toHaveLength(1); expect(result.data.provides.capabilities[0]?.name).toBe('dns_external'); expect(result.data.provides.capabilities[0]?.data).toHaveProperty('nameserver'); } }); test('should validate caddy manifest with capability consumer', () => { const yaml = ` ${CONTRACT_LINE} id: caddy name: Caddy Web Server version: 1.0.0 requires: capabilities: - name: dns_external version: "^1.0.0" provides: capabilities: - name: web version: 1.0.0 data: reverse_proxy_host: "$self:container_ip" variables: owns: - name: container_ip type: string required: true source: user imports: - name: dns_nameserver source: capability from: dns_external.nameserver - name: dns_zone source: capability from: dns_external.zone `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.requires.capabilities).toHaveLength(1); expect(result.data.requires.capabilities[0]?.name).toBe('dns_external'); expect(result.data.variables.imports).toHaveLength(2); expect(result.data.variables.imports[0]?.from).toBe('dns_external.nameserver'); } }); test('should reject empty manifest', () => { const result = validateManifest(''); expect(result.success).toBe(false); if (!result.success) { expect(result.errors[0]?.message).toContain('cannot be empty'); } }); test('should reject manifest missing celilo_contract', () => { const yaml = ` id: test-module name: Test version: 1.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(false); if (!result.success) { expect(result.errors.some((e) => e.path === 'celilo_contract')).toBe(true); } }); test('should reject manifest with unsupported celilo_contract version', () => { const yaml = ` celilo_contract: "9.9" id: test-module name: Test version: 1.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should reject manifest with unknown top-level key', () => { const yaml = ` ${CONTRACT_LINE} id: test name: Test version: 1.0.0 poop: 12 `; const result = validateManifest(yaml); expect(result.success).toBe(false); if (!result.success) { expect(result.errors.some((e) => e.message.toLowerCase().includes('unrecognized'))).toBe( true, ); } }); test('should reject manifest with unknown hook name', () => { const yaml = ` ${CONTRACT_LINE} id: test name: Test version: 1.0.0 hooks: on_poop: script: ./scripts/poop.ts `; const result = validateManifest(yaml); expect(result.success).toBe(false); if (!result.success) { expect(result.errors.some((e) => e.message.toLowerCase().includes('unrecognized'))).toBe( true, ); } }); test('should reject manifest with missing id', () => { const yaml = ` ${CONTRACT_LINE} name: Test Module version: 1.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(false); if (!result.success) { expect(result.errors.some((e) => e.path === 'id')).toBe(true); } }); test('should reject manifest with invalid id format', () => { const yaml = ` ${CONTRACT_LINE} id: Invalid_ID_With_Caps name: Test Module version: 1.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(false); if (!result.success) { expect(result.errors.some((e) => e.path === 'id')).toBe(true); expect(result.errors.some((e) => e.message.includes('lowercase'))).toBe(true); } }); test('should reject manifest with invalid version format', () => { const yaml = ` ${CONTRACT_LINE} id: test-module name: Test Module version: 1.0 `; const result = validateManifest(yaml); expect(result.success).toBe(false); if (!result.success) { const versionError = result.errors.find((e) => e.path === 'version'); expect(versionError).toBeDefined(); expect(versionError?.message).toBeTruthy(); } }); test('should reject malformed YAML', () => { const yaml = ` ${CONTRACT_LINE} id: test name: Test version: 1.0.0 bad_yaml: [unclosed array `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should validate manifest with lifecycle hooks', () => { const yaml = ` ${CONTRACT_LINE} id: test-module name: Test Module version: 1.0.0 hooks: on_install: script: ./scripts/install.sh timeout: 300 on_uninstall: script: ./scripts/uninstall.sh health_check: script: ./scripts/health.sh timeout: 60 `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.hooks?.on_install?.script).toBe('./scripts/install.sh'); expect(result.data.hooks?.on_install?.timeout).toBe(300); expect(result.data.hooks?.health_check?.script).toBe('./scripts/health.sh'); } }); test('should validate manifest with build script', () => { const yaml = ` ${CONTRACT_LINE} id: caddy-custom name: Custom Caddy Build version: 1.0.0 build: script: ./build.sh artifacts: - caddy - caddy.service `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.build?.script).toBe('./build.sh'); expect(result.data.build?.artifacts).toHaveLength(2); } }); test('should reject build artifacts with absolute paths', () => { const yaml = ` ${CONTRACT_LINE} id: test-module name: Test version: 1.0.0 build: script: build.sh artifacts: - /usr/local/bin/binary `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should reject build artifacts with tilde paths', () => { const yaml = ` ${CONTRACT_LINE} id: test-module name: Test version: 1.0.0 build: script: build.sh artifacts: - ~/build/binary `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should reject build artifacts with path traversal', () => { const yaml = ` ${CONTRACT_LINE} id: test-module name: Test version: 1.0.0 build: script: build.sh artifacts: - ../../../etc/passwd `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should reject build artifacts with leading ./', () => { const yaml = ` ${CONTRACT_LINE} id: test-module name: Test version: 1.0.0 build: script: build.sh artifacts: - ./build/binary `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should accept valid relative build artifact paths', () => { const yaml = ` ${CONTRACT_LINE} id: test-module name: Test version: 1.0.0 build: script: build.sh artifacts: - build/binary - output/compiled.bin - dist/package.tar.gz `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.build?.artifacts).toEqual([ 'build/binary', 'output/compiled.bin', 'dist/package.tar.gz', ]); } }); test('should validate manifest with VM resources under requires.system (canonical name)', () => { const yaml = ` ${CONTRACT_LINE} id: grafana name: Grafana version: 1.0.0 requires: capabilities: [] system: cpu: 2 memory: 2048 disk: 20 storage: local-lvm zone: app `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.requires.system?.cpu).toBe(2); expect(result.data.requires.system?.memory).toBe(2048); expect(result.data.requires.system?.zone).toBe('app'); // getSingularSystemSpec resolves the canonical field… expect(getSingularSystemSpec(result.data)?.cpu).toBe(2); } }); test('should validate manifest with minimal VM resources', () => { const yaml = ` ${CONTRACT_LINE} id: simple-service name: Simple Service version: 1.0.0 requires: capabilities: [] system: zone: dmz `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.requires.system?.zone).toBe('dmz'); expect(result.data.requires.system?.cpu).toBeUndefined(); expect(result.data.requires.system?.memory).toBeUndefined(); } }); test('should reject negative CPU value', () => { const yaml = ` ${CONTRACT_LINE} id: bad-module name: Bad Module version: 1.0.0 requires: capabilities: [] system: cpu: -2 zone: app `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should reject invalid zone value', () => { const yaml = ` ${CONTRACT_LINE} id: bad-module name: Bad Module version: 1.0.0 requires: capabilities: [] system: zone: invalid `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('should reject non-integer CPU value', () => { const yaml = ` ${CONTRACT_LINE} id: bad-module name: Bad Module version: 1.0.0 requires: capabilities: [] system: cpu: 2.5 zone: app `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); }); describe('optional.capabilities (HOOK_API_V2 D3)', () => { test('parses through validateManifest as a top-level field', () => { const yaml = ` ${CONTRACT_LINE} id: caddy name: Caddy version: 1.0.0 requires: capabilities: - name: public_web version: 1.0.0 optional: capabilities: - name: dns_registrar version: 2.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.optional?.capabilities).toHaveLength(1); expect(result.data.optional?.capabilities[0]?.name).toBe('dns_registrar'); expect(result.data.optional?.capabilities[0]?.version).toBe('2.0.0'); } }); test('is undefined when not declared (truly optional at schema level)', () => { const yaml = ` ${CONTRACT_LINE} id: standalone name: Standalone version: 1.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.optional).toBeUndefined(); } }); }); describe('validateCapabilityNames', () => { test('accepts known capability names in requires', () => { const manifest = { celilo_contract: '1.0' as const, id: 'lunacycle', name: 'LunaCycle', version: '1.0.0', requires: { capabilities: [ { name: 'public_web', version: '^1.0.0' }, { name: 'idp', version: '^1.0.0' }, ], }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; expect(validateCapabilityNames(manifest)).toBeNull(); }); test('accepts known capability names in optional', () => { const manifest = { celilo_contract: '1.0' as const, id: 'caddy', name: 'Caddy', version: '1.0.0', requires: { capabilities: [] }, optional: { capabilities: [{ name: 'dns_registrar', version: '^2.0.0' }], }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; expect(validateCapabilityNames(manifest)).toBeNull(); }); test('rejects unknown capability name in requires', () => { const manifest = { celilo_contract: '1.0' as const, id: 'broken', name: 'Broken', version: '1.0.0', requires: { capabilities: [{ name: 'dns_register', version: '^1.0.0' }], // typo }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; const result = validateCapabilityNames(manifest); expect(result).not.toBeNull(); if (result) { expect(result.errors).toHaveLength(1); expect(result.errors[0]?.path).toBe('requires.capabilities.dns_register'); expect(result.errors[0]?.message).toContain("Unknown capability 'dns_register'"); expect(result.errors[0]?.message).toContain('dns_registrar'); } }); test('rejects unknown capability name in optional', () => { const manifest = { celilo_contract: '1.0' as const, id: 'broken', name: 'Broken', version: '1.0.0', requires: { capabilities: [] }, optional: { capabilities: [{ name: 'monitoring', version: '^1.0.0' }], }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; const result = validateCapabilityNames(manifest); expect(result).not.toBeNull(); if (result) { expect(result.errors).toHaveLength(1); expect(result.errors[0]?.path).toBe('optional.capabilities.monitoring'); } }); test('reports both required and optional unknowns in one pass', () => { const manifest = { celilo_contract: '1.0' as const, id: 'broken', name: 'Broken', version: '1.0.0', requires: { capabilities: [{ name: 'bad_required', version: '^1.0.0' }], }, optional: { capabilities: [{ name: 'bad_optional', version: '^1.0.0' }], }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; const result = validateCapabilityNames(manifest); expect(result).not.toBeNull(); if (result) { expect(result.errors).toHaveLength(2); } }); test('passes when no capabilities declared', () => { const manifest = { celilo_contract: '1.0' as const, id: 'standalone', name: 'Standalone', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; expect(validateCapabilityNames(manifest)).toBeNull(); }); }); describe('validateCapabilityRequirements', () => { test('should pass when all required capabilities are available', () => { const manifest = { celilo_contract: '1.0' as const, id: 'caddy', name: 'Caddy', version: '1.0.0', requires: { capabilities: [{ name: 'dns_external', version: '^1.0.0' }], }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; const result = validateCapabilityRequirements(manifest, ['dns_external', 'idp']); expect(result).toBeNull(); }); test('should fail when required capability is missing', () => { const manifest = { celilo_contract: '1.0' as const, id: 'caddy', name: 'Caddy', version: '1.0.0', requires: { capabilities: [ { name: 'dns_external', version: '^1.0.0' }, { name: 'idp', version: '^1.0.0' }, ], }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; const result = validateCapabilityRequirements(manifest, ['dns_external']); expect(result).not.toBeNull(); if (result) { expect(result.errors).toHaveLength(1); expect(result.errors[0]?.message).toContain('idp'); } }); test('should pass when no capabilities required', () => { const manifest = { celilo_contract: '1.0' as const, id: 'homebridge', name: 'Homebridge', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; const result = validateCapabilityRequirements(manifest, []); expect(result).toBeNull(); }); }); describe('validateVariableSources', () => { test('should pass when imported capability variables reference required capabilities', () => { const manifest = { celilo_contract: '1.0' as const, id: 'caddy', name: 'Caddy', version: '1.0.0', requires: { capabilities: [{ name: 'dns_external', version: '^1.0.0' }], }, provides: { capabilities: [] }, variables: { owns: [], imports: [ { name: 'dns_nameserver', source: 'capability' as const, from: 'dns_external.nameserver', }, { name: 'dns_zone', source: 'capability' as const, from: 'dns_external.zone' }, ], }, }; const result = validateVariableSources(manifest); expect(result).toBeNull(); }); test('should fail when imported capability variable references unrequired capability', () => { const manifest = { celilo_contract: '1.0' as const, id: 'caddy', name: 'Caddy', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [ { name: 'dns_nameserver', source: 'capability' as const, from: 'dns_external.nameserver', }, ], }, }; const result = validateVariableSources(manifest); expect(result).not.toBeNull(); if (result) { expect(result.errors).toHaveLength(1); expect(result.errors[0]?.message).toContain('does not declare it'); } }); test('should pass when imported capability variable references an optional capability', () => { const manifest = { celilo_contract: '1.0' as const, id: 'caddy', name: 'Caddy', version: '1.0.0', requires: { capabilities: [] }, optional: { capabilities: [{ name: 'dns_registrar', version: '^2.0.0' }], }, provides: { capabilities: [] }, variables: { owns: [], imports: [ { name: 'dns_nameserver', source: 'capability' as const, from: 'dns_registrar.nameserver', }, ], }, }; const result = validateVariableSources(manifest); expect(result).toBeNull(); }); test('should fail when imported capability variable has invalid reference format', () => { const manifest = { celilo_contract: '1.0' as const, id: 'test', name: 'Test', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [{ name: 'bad_var', source: 'capability' as const, from: '' }], }, }; const result = validateVariableSources(manifest); expect(result).not.toBeNull(); if (result) { expect(result.errors[0]?.message).toContain('invalid capability reference'); } }); test('should pass when no variables import capabilities', () => { const manifest = { celilo_contract: '1.0' as const, id: 'homebridge', name: 'Homebridge', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'target_ip', type: 'string' as const, required: true, source: 'user' as const, }, ], imports: [], }, }; const result = validateVariableSources(manifest); expect(result).toBeNull(); }); }); describe('validateDeriveFromSources', () => { test('should pass when capability source uses $capability: prefix', () => { const manifest = { celilo_contract: '1.0' as const, id: 'lunacycle', name: 'LunaCycle', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string' as const, required: false, source: 'capability' as const, derive_from: '$capability:dns_registrar.primary_domain', }, ], imports: [], }, }; const result = validateDeriveFromSources(manifest); expect(result).toBeNull(); }); test('should reject capability source with $system: prefix in derive_from (regression)', () => { // This is the original lunacycle bug that motivated D7. const manifest = { celilo_contract: '1.0' as const, id: 'lunacycle', name: 'LunaCycle', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string' as const, required: false, source: 'capability' as const, derive_from: '$system:primary_domain', }, ], imports: [], }, }; const result = validateDeriveFromSources(manifest); expect(result).not.toBeNull(); if (result) { expect(result.errors[0]?.path).toContain('primary_domain'); expect(result.errors[0]?.message).toContain('$system:'); } }); test('should pass for variables without derive_from', () => { const manifest = { celilo_contract: '1.0' as const, id: 'test', name: 'Test', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'app_port', type: 'integer' as const, required: false, source: 'user' as const, default: 3000, }, ], imports: [], }, }; const result = validateDeriveFromSources(manifest); expect(result).toBeNull(); }); test('should accept $self: references regardless of source', () => { // $self: points at another variable in this same manifest, so it // doesn't introduce the cross-context mismatch the validator exists // to catch (e.g. authentik's auth_url derives from $self:domain). const manifest = { celilo_contract: '1.0' as const, id: 'authentik', name: 'Authentik', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'domain', type: 'string' as const, required: true, source: 'user' as const, }, { name: 'auth_url', type: 'string' as const, required: false, source: 'capability' as const, derive_from: 'https://auth.$self:domain', }, ], imports: [], }, }; const result = validateDeriveFromSources(manifest); expect(result).toBeNull(); }); test('should still reject mixed $self: and $system: when source is capability', () => { // Self-refs are exempt, but other foreign sources still trigger the // mismatch error. This guards against accidentally green-lighting // "$self:foo and $system:bar" combos. const manifest = { celilo_contract: '1.0' as const, id: 'test', name: 'Test', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'mixed', type: 'string' as const, required: false, source: 'capability' as const, derive_from: '$self:domain/$system:primary_domain', }, ], imports: [], }, }; const result = validateDeriveFromSources(manifest); expect(result).not.toBeNull(); if (result) { expect(result.errors[0]?.message).toContain('$system:'); expect(result.errors[0]?.message).not.toContain('$self:'); } }); }); describe('validateHookContract', () => { test('should pass when all declared hooks are in v1 contract', () => { const manifest = { celilo_contract: '1.0' as const, id: 'test', name: 'Test', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, hooks: { on_install: { script: './install.ts' }, on_backup: { script: './backup.ts' }, }, }; const result = validateHookContract(manifest); expect(result).toBeNull(); }); test('should pass for manifests with no hooks block', () => { const manifest = { celilo_contract: '1.0' as const, id: 'test', name: 'Test', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; const result = validateHookContract(manifest); expect(result).toBeNull(); }); }); describe('validateProvidesNoCrossCapabilityRefs', () => { test('should pass when capability data has no cross-capability references', () => { const manifest = { celilo_contract: '1.0' as const, id: 'authentik', name: 'Authentik', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [ { name: 'idp', version: '1.0.0', data: { auth_url: '$self:auth_url', api_url: 'http://$self:container_ip:9000', }, }, ], }, variables: { owns: [], imports: [] }, }; const result = validateProvidesNoCrossCapabilityRefs(manifest); expect(result).toBeNull(); }); test('should reject capability data containing $capability: reference (D9 firm rule)', () => { const manifest = { celilo_contract: '1.0' as const, id: 'authentik', name: 'Authentik', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [ { name: 'idp', version: '1.0.0', data: { issuer_url: 'https://auth.$capability:dns_registrar.primary_domain', }, }, ], }, variables: { owns: [], imports: [] }, }; const result = validateProvidesNoCrossCapabilityRefs(manifest); expect(result).not.toBeNull(); if (result) { expect(result.errors[0]?.path).toContain('issuer_url'); expect(result.errors[0]?.message).toContain('$capability:'); } }); }); describe('secret generate field', () => { test('should parse secrets with generate config', () => { const yaml = ` ${CONTRACT_LINE} id: test-secrets name: Test Secrets version: 1.0.0 secrets: declares: - name: auto_secret type: string required: true description: "Auto-generated secret" generate: method: random length: 50 encoding: base64 - name: user_secret type: string required: true description: "User-provided secret" `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { const secrets = result.data.secrets?.declares; if (!secrets) throw new Error('secrets.declares should exist'); expect(secrets).toHaveLength(2); const autoSecret = secrets.find((s) => s.name === 'auto_secret'); expect(autoSecret?.generate).toBeDefined(); expect(autoSecret?.generate?.method).toBe('random'); expect(autoSecret?.generate?.length).toBe(50); expect(autoSecret?.generate?.encoding).toBe('base64'); const userSecret = secrets.find((s) => s.name === 'user_secret'); expect(userSecret?.generate).toBeUndefined(); } }); test('should use defaults for generate fields', () => { const yaml = ` ${CONTRACT_LINE} id: test-defaults name: Test Defaults version: 1.0.0 secrets: declares: - name: minimal_generate type: string required: true generate: {} `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { const declares = result.data.secrets?.declares; if (!declares) throw new Error('secrets.declares should exist'); const secret = declares[0]; expect(secret.generate).toBeDefined(); expect(secret.generate?.method).toBe('random'); expect(secret.generate?.length).toBe(32); expect(secret.generate?.encoding).toBe('base64'); } }); test('should reject invalid generate encoding', () => { const yaml = ` ${CONTRACT_LINE} id: test-invalid name: Test Invalid version: 1.0.0 secrets: declares: - name: bad_secret type: string required: true generate: encoding: sha256 `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); }); describe('subscriptions field', () => { test('manifests without subscriptions still validate', () => { const yaml = ` ${CONTRACT_LINE} id: test name: Test version: 1.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.subscriptions).toBeUndefined(); } }); test('parses a valid subscriptions block', () => { const yaml = ` ${CONTRACT_LINE} id: test name: Test version: 1.0.0 subscriptions: - name: smoke-after-deploy pattern: deploy.completed.$self handler: bun \${MODULE_PATH}/celilo/scripts/smoke.ts timeout_ms: 120000 - name: cert-rotated pattern: cert.rotated handler: echo max_attempts: 5 `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.subscriptions).toHaveLength(2); expect(result.data.subscriptions?.[0].name).toBe('smoke-after-deploy'); expect(result.data.subscriptions?.[0].timeout_ms).toBe(120000); expect(result.data.subscriptions?.[1].max_attempts).toBe(5); } }); test('rejects subscription names with invalid characters', () => { const yaml = ` ${CONTRACT_LINE} id: test name: Test version: 1.0.0 subscriptions: - name: SmokeAfterDeploy pattern: deploy.$self handler: echo `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('requires pattern and handler to be non-empty', () => { const yaml = ` ${CONTRACT_LINE} id: test name: Test version: 1.0.0 subscriptions: - name: smoke pattern: "" handler: echo `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); }); describe('base_module_aspect field', () => { test('accepts a minimal valid aspect block', () => { const yaml = ` ${CONTRACT_LINE} id: knot-unbound-internal name: Internal DNS version: 1.0.0 base_module_aspect: ansible_role: dns-client-config applicable_zones: [app] triggers: [on_install] `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.base_module_aspect?.ansible_role).toBe('dns-client-config'); expect(result.data.base_module_aspect?.applicable_zones).toEqual(['app']); expect(result.data.base_module_aspect?.triggers).toEqual(['on_install']); } }); test('defaults triggers to [on_install] when omitted', () => { const yaml = ` ${CONTRACT_LINE} id: knot-unbound-internal name: Internal DNS version: 1.0.0 base_module_aspect: ansible_role: dns-client-config applicable_zones: [dmz, app, secure, internal] `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.base_module_aspect?.triggers).toEqual(['on_install']); } }); test('accepts the full trigger set', () => { const yaml = ` ${CONTRACT_LINE} id: knot-unbound-internal name: Internal DNS version: 1.0.0 base_module_aspect: ansible_role: dns-client-config applicable_zones: [app] triggers: - on_install - on_new_system_in_zone - on_aspect_change - on_module_config_change - on_capability_data_change `; const result = validateManifest(yaml); expect(result.success).toBe(true); }); test('rejects empty applicable_zones', () => { const yaml = ` ${CONTRACT_LINE} id: knot-unbound-internal name: Internal DNS version: 1.0.0 base_module_aspect: ansible_role: dns-client-config applicable_zones: [] triggers: [on_install] `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('rejects missing ansible_role', () => { const yaml = ` ${CONTRACT_LINE} id: knot-unbound-internal name: Internal DNS version: 1.0.0 base_module_aspect: applicable_zones: [app] triggers: [on_install] `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('rejects unknown trigger names', () => { const yaml = ` ${CONTRACT_LINE} id: knot-unbound-internal name: Internal DNS version: 1.0.0 base_module_aspect: ansible_role: dns-client-config applicable_zones: [app] triggers: [on_install, on_nonsense] `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('rejects empty triggers when explicitly set', () => { const yaml = ` ${CONTRACT_LINE} id: knot-unbound-internal name: Internal DNS version: 1.0.0 base_module_aspect: ansible_role: dns-client-config applicable_zones: [app] triggers: [] `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); test('omitting the block entirely is valid (most modules)', () => { const yaml = ` ${CONTRACT_LINE} id: homebridge name: Homebridge version: 1.0.0 `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) { expect(result.data.base_module_aspect).toBeUndefined(); } }); test('rejects unknown fields inside the block (strict)', () => { const yaml = ` ${CONTRACT_LINE} id: knot-unbound-internal name: Internal DNS version: 1.0.0 base_module_aspect: ansible_role: dns-client-config applicable_zones: [app] triggers: [on_install] unknown_extra_field: foo `; const result = validateManifest(yaml); expect(result.success).toBe(false); }); }); describe('submodules (openspec/changes/submodules D1, D9)', () => { const parent = (over: Record = {}) => ({ celilo_contract: '1.0' as const, id: 'forgejo', name: 'Forgejo', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, ...over, }); const submodule = (over: Record = {}) => ({ celilo_contract: '1.0' as const, id: 'runner', name: 'Forgejo Runner', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, ...over, }); describe('validateSubmoduleDeclaration', () => { test('a module declaring no submodules is unaffected', () => { expect(validateSubmoduleDeclaration(parent())).toBeNull(); }); test('accepts a plain list of names', () => { expect(validateSubmoduleDeclaration(parent({ submodules: ['runner'] }))).toBeNull(); }); test('rejects a duplicate name', () => { const result = validateSubmoduleDeclaration(parent({ submodules: ['runner', 'runner'] })); expect(result).not.toBeNull(); expect(result?.errors[0]?.message).toContain('more than once'); }); test('rejects a submodule sharing its parent id', () => { const result = validateSubmoduleDeclaration(parent({ submodules: ['forgejo'] })); expect(result).not.toBeNull(); expect(result?.errors[0]?.message).toContain("parent module's id"); }); }); describe('validateSubmoduleManifest', () => { test('accepts an ordinary submodule manifest', () => { expect(validateSubmoduleManifest(parent(), 'runner', submodule())).toBeNull(); }); test('rejects an id that disagrees with its directory', () => { const result = validateSubmoduleManifest(parent(), 'runner', submodule({ id: 'builder' })); expect(result).not.toBeNull(); expect(result?.errors[0]?.message).toContain('does not match its directory'); }); // D9: a capability resolves to ONE provider, and celilo's two bespoke // multi-provider rules RESOLVE rather than fail, so N instances would bind // a consumer to whichever was created first — silently. test('rejects a submodule that provides a capability', () => { const result = validateSubmoduleManifest( parent(), 'runner', submodule({ provides: { capabilities: [{ name: 'source_forge', version: '1.0.0', data: {} }] }, }), ); expect(result).not.toBeNull(); expect(result?.errors[0]?.message).toContain('may not provide a capability'); expect(result?.errors[0]?.message).toContain('source_forge'); }); test('a submodule may still REQUIRE capabilities', () => { const result = validateSubmoduleManifest( parent(), 'runner', submodule({ requires: { capabilities: [{ name: 'source_forge', version: '1.0.0' }] } }), ); expect(result).toBeNull(); }); test('rejects nesting — ownership is one level deep', () => { const result = validateSubmoduleManifest( parent(), 'runner', submodule({ submodules: ['nested'] }), ); expect(result).not.toBeNull(); expect(result?.errors[0]?.message).toContain('may not declare submodules of its own'); }); test('reports every problem at once rather than stopping at the first', () => { const result = validateSubmoduleManifest( parent(), 'runner', submodule({ id: 'builder', provides: { capabilities: [{ name: 'source_forge', version: '1.0.0', data: {} }] }, submodules: ['nested'], }), ); expect(result?.errors).toHaveLength(3); }); }); describe('schema', () => { test('parses a manifest declaring submodules', () => { const yaml = ` celilo_contract: "1.0" id: forgejo name: Forgejo version: 1.0.0 submodules: - runner requires: capabilities: [] `; const result = validateManifest(yaml); expect(result.success).toBe(true); if (result.success) expect(result.data.submodules).toEqual(['runner']); }); test('rejects a non-kebab-case submodule name', () => { const yaml = ` celilo_contract: "1.0" id: forgejo name: Forgejo version: 1.0.0 submodules: - Runner_One requires: capabilities: [] `; const result = validateManifest(yaml); expect(result.success).toBe(false); if (!result.success) expect(result.errors[0]?.message).toContain('kebab-case'); }); }); }); describe('health_waiver (openspec/changes/health-waiver-mechanism)', () => { const base = `\n${CONTRACT_LINE}\nid: namecheap\nname: Namecheap\nversion: 1.0.0\n`; test('accepts a well-formed waiver and keeps all three fields', () => { const result = validateManifest( `${base}\nhealth_waiver:\n reason: "API-only: a real check is hard and may not be worth forcing"\n by: peba\n at: "2026-09-09"\n`, ); expect(result.success).toBe(true); if (result.success) { expect(result.data.health_waiver).toEqual({ reason: 'API-only: a real check is hard and may not be worth forcing', by: 'peba', at: '2026-09-09', }); } }); test('rejects an unknown field inside the waiver (strict)', () => { const result = validateManifest( `${base}\nhealth_waiver:\n reason: r\n by: peba\n at: "2026-09-09"\n expires: "2027-01-01"\n`, ); expect(result.success).toBe(false); if (!result.success) { expect(result.errors.some((e) => e.message.toLowerCase().includes('unrecognized'))).toBe( true, ); } }); test('rejects a waiver with no reason', () => { const result = validateManifest(`${base}\nhealth_waiver:\n by: peba\n at: "2026-09-09"\n`); expect(result.success).toBe(false); }); test('rejects a waiver with no by', () => { const result = validateManifest(`${base}\nhealth_waiver:\n reason: r\n at: "2026-09-09"\n`); expect(result.success).toBe(false); }); test('rejects a waiver whose at is not a date', () => { const result = validateManifest( `${base}\nhealth_waiver:\n reason: r\n by: peba\n at: not-a-date\n`, ); expect(result.success).toBe(false); }); });