// tests/validators/profile-validator.test.mjs // SC #1, #2, #3: profile-validator validates lib/profiles/{economy,balanced,premium}.yaml // (innebygde profiler) plus rejects invalid models and invalid enum types. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { validateProfile, validateProfileContent, PROFILE_REQUIRED_FIELDS, PROFILE_REQUIRED_PHASES, } from '../../lib/validators/profile-validator.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(__dirname, '..', '..'); // SC #1: alle 3 innebygde profiler grønne for (const profileName of ['economy', 'balanced', 'premium']) { test(`SC #1: lib/profiles/${profileName}.yaml validates clean`, () => { const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', `${profileName}.yaml`)); assert.equal(r.valid, true, `expected valid, got errors: ${JSON.stringify(r.errors, null, 2)}`); assert.equal(r.errors.length, 0); // Spot-check: all 6 phases present const phases = r.parsed.frontmatter.phase_models.map(p => p.phase); for (const required of PROFILE_REQUIRED_PHASES) { assert.ok(phases.includes(required), `${profileName} missing phase: ${required}`); } }); } // SC #2: PROFILE_INVALID_MODEL fired when phase_models[N].model not in allowlist test('SC #2: profile-invalid-model.yaml rejected with PROFILE_INVALID_MODEL at phase_models[2].model', () => { const r = validateProfile(join(REPO_ROOT, 'tests', 'fixtures', 'profile-invalid-model.yaml')); assert.equal(r.valid, false); const found = r.errors.find(e => e.code === 'PROFILE_INVALID_MODEL'); assert.ok(found, `expected PROFILE_INVALID_MODEL, got: ${JSON.stringify(r.errors)}`); assert.equal(found.location, 'phase_models[2].model', `expected location phase_models[2].model, got ${found.location}`); assert.match(found.message, /gpt-4/); }); // SC #3: PROFILE_INVALID_ENUM for wrong-type values test('SC #3: profile-invalid-enum.yaml rejected with PROFILE_INVALID_ENUM (external_research_enabled is string)', () => { const r = validateProfile(join(REPO_ROOT, 'tests', 'fixtures', 'profile-invalid-enum.yaml')); assert.equal(r.valid, false); const found = r.errors.find(e => e.code === 'PROFILE_INVALID_ENUM' && /external_research_enabled/.test(e.message)); assert.ok(found, `expected PROFILE_INVALID_ENUM for external_research_enabled, got: ${JSON.stringify(r.errors)}`); assert.match(found.message, /boolean/); }); // VOYAGE_ALLOW_HAIKU env-var allows haiku model test('VOYAGE_ALLOW_HAIKU=1 allows haiku in phase_models', () => { const haikuProfile = `--- profile_version: "1.0" name: haiku-allowed phase_models: - phase: brief model: haiku - phase: research model: sonnet - phase: plan model: opus - phase: execute model: sonnet - phase: review model: opus - phase: continue model: sonnet parallel_agents_min: 2 parallel_agents_max: 4 external_research_enabled: false brief_reviewer_iter_cap: 1 --- `; // Default env: haiku rejected const denied = validateProfileContent(haikuProfile, { env: { /* no VOYAGE_ALLOW_HAIKU */ } }); assert.equal(denied.valid, false); const haikuErr = denied.errors.find(e => e.code === 'PROFILE_INVALID_MODEL' && /haiku/.test(e.message)); assert.ok(haikuErr, `expected haiku rejection: ${JSON.stringify(denied.errors)}`); assert.match(haikuErr.message, /VOYAGE_ALLOW_HAIKU/); // With opt-in: haiku accepted const allowed = validateProfileContent(haikuProfile, { env: { VOYAGE_ALLOW_HAIKU: '1' } }); assert.equal(allowed.valid, true, `expected valid with VOYAGE_ALLOW_HAIKU=1, got: ${JSON.stringify(allowed.errors)}`); }); // Required fields presence test('PROFILE_MISSING_FIELD when name absent', () => { const r = validateProfileContent(`--- profile_version: "1.0" phase_models: - phase: brief model: sonnet - phase: research model: sonnet - phase: plan model: opus - phase: execute model: sonnet - phase: review model: opus - phase: continue model: sonnet parallel_agents_min: 2 parallel_agents_max: 4 external_research_enabled: false brief_reviewer_iter_cap: 1 --- `); assert.equal(r.valid, false); const found = r.errors.find(e => e.code === 'PROFILE_MISSING_FIELD' && /name/.test(e.message)); assert.ok(found, `expected PROFILE_MISSING_FIELD for name, got: ${JSON.stringify(r.errors)}`); }); // PROFILE_NOT_FOUND for missing file test('PROFILE_NOT_FOUND for non-existent file', () => { const r = validateProfile('/tmp/does-not-exist-profile-xyz.yaml'); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROFILE_NOT_FOUND')); }); // REQUIRED_FIELDS frontmatter contract drift-pin test('PROFILE_REQUIRED_FIELDS export drift-pin', () => { assert.deepEqual( [...PROFILE_REQUIRED_FIELDS].sort(), ['brief_reviewer_iter_cap', 'external_research_enabled', 'name', 'parallel_agents_max', 'parallel_agents_min', 'phase_models'].sort(), 'PROFILE_REQUIRED_FIELDS contract drift — pin contract', ); }); test('PROFILE_REQUIRED_PHASES export drift-pin', () => { assert.deepEqual( [...PROFILE_REQUIRED_PHASES].sort(), ['brief', 'research', 'plan', 'execute', 'review', 'continue'].sort(), 'PROFILE_REQUIRED_PHASES contract drift — pin contract', ); });