import { describe, expect, test } from 'bun:test'; import type { ModuleManifest, VariableDeclare } from '../manifest/schema'; import { applyDeclarativeDerivations, resolveDeclarativeDerivation, } from './declarative-derivation'; import type { ResolutionContext } from './types'; describe('resolveDeclarativeDerivation', () => { describe('system config references', () => { test('resolves $system:key pattern', () => { const variable: VariableDeclare = { name: 'primary_domain', type: 'string', required: false, source: 'user', derive_from: '$system:primary_domain', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: {}, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('example.com'); }); test('resolves nested system config with dots', () => { const variable: VariableDeclare = { name: 'dmz_subnet', type: 'string', required: false, source: 'user', derive_from: '$system:network.dmz.subnet', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: { 'network.dmz.subnet': '10.0.10.0/24' }, systemSecrets: {}, secrets: {}, capabilities: {}, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('10.0.10.0/24'); }); test('resolves a system key containing hyphens', () => { // celilo's zone names are kebab-case, so shipped manifests contain // `$system:network.control-plane-vpn.subnet` and // `$system:network.secure-mgmt.subnet`. While the match stopped at the // first hyphen these looked up `network.control` / `network.secure`, // threw, and — being optional — resolved to nothing in silence. That is // half of the 2026-08-14 DNS outage. const variable: VariableDeclare = { name: 'vpn_subnet', type: 'string', required: false, source: 'system', derive_from: '$system:network.control-plane-vpn.subnet', }; const context: ResolutionContext = { moduleId: 'technitium', selfConfig: {}, systemConfig: { 'network.control-plane-vpn.subnet': '10.255.255.0/24' }, systemSecrets: {}, secrets: {}, capabilities: {}, }; expect(resolveDeclarativeDerivation(variable, context)).toBe('10.255.255.0/24'); }); test('names the whole hyphenated key when it is missing', () => { // The message has to name the key the manifest asked for. Reporting // `network.secure` for a manifest that says `network.secure-mgmt` sends // the reader looking for a key that was never requested. const variable: VariableDeclare = { name: 'secure_mgmt_subnet', type: 'string', required: true, source: 'system', derive_from: '$system:network.secure-mgmt.subnet', }; const context: ResolutionContext = { moduleId: 'technitium', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; expect(() => resolveDeclarativeDerivation(variable, context)).toThrow( "Missing system config: network.secure-mgmt.subnet (required by variable 'secure_mgmt_subnet')", ); }); test('throws on missing system config', () => { const variable: VariableDeclare = { name: 'primary_domain', type: 'string', required: false, source: 'user', derive_from: '$system:primary_domain', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; expect(() => resolveDeclarativeDerivation(variable, context)).toThrow( "Missing system config: primary_domain (required by variable 'primary_domain')", ); }); }); describe('variable interpolation', () => { test('resolves {variable} pattern', () => { const variable: VariableDeclare = { name: 'vps_hostname', type: 'string', required: false, source: 'user', derive_from: '{hostname}.example.com', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { hostname: 'dns-ext' }, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('dns-ext.example.com'); }); test('resolves multiple variable interpolations', () => { const variable: VariableDeclare = { name: 'vps_hostname', type: 'string', required: false, source: 'user', derive_from: '{hostname}.{primary_domain}', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { hostname: 'dns-ext', primary_domain: 'example.com' }, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('dns-ext.example.com'); }); test('throws on missing variable', () => { const variable: VariableDeclare = { name: 'vps_hostname', type: 'string', required: false, source: 'user', derive_from: '{hostname}.{primary_domain}', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { hostname: 'dns-ext' }, // missing primary_domain systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; expect(() => resolveDeclarativeDerivation(variable, context)).toThrow( "Missing variable: primary_domain (required by variable 'vps_hostname')", ); }); }); describe('capability references', () => { test('resolves $capability:name.path pattern', () => { const variable: VariableDeclare = { name: 'dns_server_ip', type: 'string', required: false, source: 'user', derive_from: '$capability:dns_external.server.ip', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: { dns_external: { server: { ip: '192.0.2.20', }, }, }, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('192.0.2.20'); }); test('resolves nested capability path', () => { const variable: VariableDeclare = { name: 'dns_primary', type: 'string', required: false, source: 'user', derive_from: '$capability:dns_external.server.ip.primary', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: { dns_external: { server: { ip: { primary: '192.0.2.20', secondary: '188.166.157.3', }, }, }, }, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('192.0.2.20'); }); test('throws on missing capability', () => { const variable: VariableDeclare = { name: 'dns_server_ip', type: 'string', required: false, source: 'user', derive_from: '$capability:dns_external.server.ip', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; expect(() => resolveDeclarativeDerivation(variable, context)).toThrow( "Missing capability: dns_external (required by variable 'dns_server_ip')", ); }); test('throws on missing capability field', () => { const variable: VariableDeclare = { name: 'dns_server_ip', type: 'string', required: false, source: 'user', derive_from: '$capability:dns_external.server.ip', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: { dns_external: { server: {}, // missing ip field }, }, }; expect(() => resolveDeclarativeDerivation(variable, context)).toThrow( "Missing capability field: dns_external.server.ip (required by variable 'dns_server_ip')", ); }); }); describe('nested variable resolution', () => { test('resolves $system: references within capability values', () => { const variable: VariableDeclare = { name: 'auth_url', type: 'string', required: false, source: 'capability', derive_from: '$capability:idp.auth_url', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: { idp: { auth_url: 'https://auth.${system:primary_domain}', }, }, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('https://auth.example.com'); }); test('resolves $system: references (unbraced) within capability values', () => { const variable: VariableDeclare = { name: 'api_url', type: 'string', required: false, source: 'capability', derive_from: '$capability:idp.api_url', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: { idp: { api_url: 'https://auth.$system:primary_domain/api', }, }, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('https://auth.example.com/api'); }); }); describe('mixed patterns', () => { test('combines system config and variable interpolation', () => { const variable: VariableDeclare = { name: 'full_url', type: 'string', required: false, source: 'user', derive_from: 'https://{hostname}.$system:primary_domain', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { hostname: 'www' }, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: {}, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('https://www.example.com'); }); test('combines all three pattern types', () => { const variable: VariableDeclare = { name: 'acme_dns', type: 'string', required: false, source: 'user', derive_from: '{hostname}.$system:primary_domain@$capability:dns_external.server.ip', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { hostname: 'caddy' }, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: { dns_external: { server: { ip: '192.0.2.20' }, }, }, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBe('caddy.example.com@192.0.2.20'); }); }); describe('no derivation', () => { test('returns undefined when derive_from is not set', () => { const variable: VariableDeclare = { name: 'hostname', type: 'string', required: false, source: 'user', }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; const result = resolveDeclarativeDerivation(variable, context); expect(result).toBeUndefined(); }); }); }); describe('applyDeclarativeDerivations', () => { describe('basic derivations', () => { test('applies single derivation', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string', required: false, source: 'user', derive_from: '$system:primary_domain', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: {}, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.primary_domain).toBe('example.com'); }); test('applies multiple derivations in order', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string', required: false, source: 'user', derive_from: '$system:primary_domain', }, { name: 'vps_hostname', type: 'string', required: false, source: 'user', derive_from: '{hostname}.{primary_domain}', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { hostname: 'dns-ext' }, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: {}, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.primary_domain).toBe('example.com'); expect(context.selfConfig.vps_hostname).toBe('dns-ext.example.com'); }); }); describe('user config precedence', () => { test('does not overwrite user-provided values', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string', required: false, source: 'user', derive_from: '$system:primary_domain', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { primary_domain: 'custom.com' }, // User provided systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: {}, }; applyDeclarativeDerivations(manifest, context); // Should keep user value expect(context.selfConfig.primary_domain).toBe('custom.com'); }); }); describe('dependency resolution', () => { test('resolves dependencies in declaration order', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ // First: derive primary_domain from system { name: 'primary_domain', type: 'string', required: false, source: 'user', derive_from: '$system:primary_domain', }, // Second: derive vps_hostname using primary_domain { name: 'vps_hostname', type: 'string', required: false, source: 'user', derive_from: '{hostname}.{primary_domain}', }, // Third: derive full_url using vps_hostname { name: 'full_url', type: 'string', required: false, source: 'user', derive_from: 'https://{vps_hostname}', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { hostname: 'dns-ext' }, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: {}, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.primary_domain).toBe('example.com'); expect(context.selfConfig.vps_hostname).toBe('dns-ext.example.com'); expect(context.selfConfig.full_url).toBe('https://dns-ext.example.com'); }); }); describe('error handling', () => { test('throws error for required variable with missing dependency', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string', required: true, // Required! source: 'user', derive_from: '$system:primary_domain', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, // Missing primary_domain systemSecrets: {}, secrets: {}, capabilities: {}, }; expect(() => applyDeclarativeDerivations(manifest, context)).toThrow( "Missing system config: primary_domain (required by variable 'primary_domain')", ); }); test('silently skips optional variable with missing dependency', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string', required: false, // Optional source: 'user', derive_from: '$system:primary_domain', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, // Missing primary_domain systemSecrets: {}, secrets: {}, capabilities: {}, }; // Should not throw applyDeclarativeDerivations(manifest, context); // Variable remains unset expect(context.selfConfig.primary_domain).toBeUndefined(); }); test('derives scalar numeric values from capability data', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'port', type: 'integer', required: false, source: 'capability', derive_from: '$capability:dns_internal.dns.knot_port', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: { dns_internal: { dns: { knot_port: 5353 } }, }, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.port).toBe('5353'); }); test('continues to skip structured derivations', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'domains', type: 'array', required: false, source: 'capability', derive_from: '$capability:dns_internal.dns.managed_domains', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: { dns_internal: { dns: { managed_domains: ['home.arpa'] } }, }, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.domains).toBeUndefined(); }); }); describe('capability data with unresolved $self: refs', () => { // This is the real-world scenario: namecheap's dns_registrar capability is stored // in the DB with raw {primary_domain: "$self:primary_domain"} by buildCapabilityData. // When lunacycle's primary_domain derives from $capability:dns_registrar.primary_domain, // it receives the string "$self:primary_domain" — which can't be resolved in lunacycle's // context. The key must remain unset rather than being poisoned with the raw template. test('does not set selfConfig when capability value still contains $self: ref', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'lunacycle', name: 'Lunacycle', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string', required: false, source: 'capability', derive_from: '$capability:dns_registrar.primary_domain', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'lunacycle', selfConfig: {}, // primary_domain not set yet systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: { // Raw capability data as stored by buildCapabilityData — $self: unresolved dns_registrar: { primary_domain: '$self:primary_domain' }, }, }; applyDeclarativeDerivations(manifest, context); // Must NOT be set to '$self:primary_domain' — that poisons downstream code expect(context.selfConfig.primary_domain).toBeUndefined(); }); test('uses existing selfConfig value when capability returns unresolved $self: ref', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'lunacycle', name: 'Lunacycle', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'primary_domain', type: 'string', required: false, source: 'capability', derive_from: '$capability:dns_registrar.primary_domain', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'lunacycle', selfConfig: { primary_domain: 'iamtheinternet.org' }, // Previously resolved value systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: { dns_registrar: { primary_domain: '$self:primary_domain' }, // Raw, unresolved }, }; applyDeclarativeDerivations(manifest, context); // Should keep the existing resolved value expect(context.selfConfig.primary_domain).toBe('iamtheinternet.org'); }); }); describe('capability data with unresolved $secret: refs', () => { // $secret: is not handled by substituteVariables (only $system:, {var}, $capability: // are resolved). So if a capability stores "$secret:api_key", it passes through // all 5 resolution passes unchanged and must be skipped. test('does not set selfConfig when capability value contains $secret: ref', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'consumer', name: 'Consumer', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'api_key', type: 'string', required: false, source: 'capability', derive_from: '$capability:provider.api_key', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'consumer', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: { provider: { api_key: '$secret:provider_api_key' }, }, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.api_key).toBeUndefined(); }); }); describe('two-level resolution via capability', () => { // Capability data contains $system: refs — the multi-pass loop in // resolveDeclarativeDerivation resolves them. This tests applyDeclarativeDerivations // end-to-end, not just resolveDeclarativeDerivation in isolation. test('resolves $system: ref inside capability value', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'caddy', name: 'Caddy', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'auth_url', type: 'string', required: false, source: 'capability', derive_from: '$capability:idp.auth_url', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'caddy', selfConfig: {}, systemConfig: { primary_domain: 'example.com' }, systemSecrets: {}, secrets: {}, capabilities: { // Capability data uses $system: — this CAN be resolved in the consumer's context idp: { auth_url: 'https://auth.$system:primary_domain' }, }, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.auth_url).toBe('https://auth.example.com'); }); }); describe('circular variable references', () => { // Both optional variables reference each other — each throws "Missing variable" // on first pass (the other isn't set yet). Both should remain unset. test('silently skips both optional variables in a circular reference', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'foo', type: 'string', required: false, source: 'user', derive_from: '{bar}', }, { name: 'bar', type: 'string', required: false, source: 'user', derive_from: '{foo}', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.foo).toBeUndefined(); expect(context.selfConfig.bar).toBeUndefined(); }); test('throws when either circular variable is required', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'foo', type: 'string', required: true, // Required — must throw source: 'user', derive_from: '{bar}', }, { name: 'bar', type: 'string', required: false, source: 'user', derive_from: '{foo}', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; expect(() => applyDeclarativeDerivations(manifest, context)).toThrow( "Missing variable: bar (required by variable 'foo')", ); }); }); describe('$self: in derive_from', () => { // $self: is the canonical reference syntax in capability data blocks // (e.g. authentik's `data.auth_url: $self:auth_url`), so it must // resolve in derive_from too — otherwise capability-sourced variables // that derive from another self variable can't be persisted. // Equivalent to the {var} syntax; reads from selfConfig. test('resolves $self: against selfConfig like {var}', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'hostname_copy', type: 'string', required: false, source: 'user', derive_from: '$self:hostname', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: { hostname: 'my-host' }, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.hostname_copy).toBe('my-host'); }); test('resolves $self: inside string interpolation (authentik auth_url shape)', () => { // Mirrors the authentik manifest pattern that motivated the fix: // auth_url derives from "https://auth.$self:domain" where domain // is another self-owned variable set by the user. const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'authentik', name: 'Authentik', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'domain', type: 'string', required: true, source: 'user', }, { name: 'auth_url', type: 'string', required: false, source: 'capability', derive_from: 'https://auth.$self:domain', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'authentik', selfConfig: { domain: 'iamtheinternet.org' }, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.auth_url).toBe('https://auth.iamtheinternet.org'); }); test('throws when $self: references an undeclared variable', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test', name: 'Test', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'derived', type: 'string', required: false, source: 'user', derive_from: '$self:nonexistent', }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; // Optional variable + missing dep → derivation fails silently per // applyDeclarativeDerivations' optional-variable rule. The error // is thrown from substituteVariables but caught by the optional // branch in applyDeclarativeDerivations, leaving selfConfig clean. applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.derived).toBeUndefined(); }); }); describe('skips', () => { test('skips variables without derive_from', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [ { name: 'hostname', type: 'string', required: true, source: 'user', // No derive_from }, ], imports: [], }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; // Should not throw or modify context applyDeclarativeDerivations(manifest, context); expect(context.selfConfig.hostname).toBeUndefined(); }); test('handles manifest with no variables', () => { const manifest: ModuleManifest = { celilo_contract: '1.0', id: 'test-module', name: 'Test Module', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, }; const context: ResolutionContext = { moduleId: 'test-module', selfConfig: {}, systemConfig: {}, systemSecrets: {}, secrets: {}, capabilities: {}, }; // Should not throw applyDeclarativeDerivations(manifest, context); expect(context.selfConfig).toEqual({}); }); }); });