// tests/lib/profile-application.test.mjs // SC #5-#9 + backward-compat edge-case for lib/profiles/resolver.mjs. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { loadProfile, resolveProfile, resolveTrekcontinueProfile, validateProfileFile, findProfilePath, } from '../../lib/profiles/resolver.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(__dirname, '..', '..'); // SC #5: loadProfile returns matrix-match for all 6 phase_models test('SC #5: loadProfile("economy") returns flattened phase_models with all 6 phases', () => { const p = loadProfile('economy'); assert.equal(p.name, 'economy'); assert.equal(p.phase_models.brief, 'sonnet'); assert.equal(p.phase_models.research, 'sonnet'); assert.equal(p.phase_models.plan, 'sonnet'); assert.equal(p.phase_models.execute, 'sonnet'); assert.equal(p.phase_models.review, 'sonnet'); assert.equal(p.phase_models.continue, 'sonnet'); assert.equal(p.parallel_agents_min, 2); assert.equal(p.parallel_agents_max, 3); assert.equal(p.external_research_enabled, false); assert.equal(p.brief_reviewer_iter_cap, 1); }); test('SC #5: loadProfile("balanced") returns mixed phase_models', () => { const p = loadProfile('balanced'); assert.equal(p.phase_models.plan, 'opus'); assert.equal(p.phase_models.review, 'opus'); assert.equal(p.phase_models.brief, 'sonnet'); assert.equal(p.phase_models.execute, 'sonnet'); }); test('SC #5: loadProfile("premium") returns all-opus', () => { const p = loadProfile('premium'); for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) { assert.equal(p.phase_models[phase], 'opus', `premium ${phase} should be opus`); } }); test('SC #5: loadProfile throws PROFILE_NOT_FOUND for unknown profile', () => { try { loadProfile('does-not-exist-xyz'); assert.fail('expected throw'); } catch (e) { assert.equal(e.cause, 'PROFILE_NOT_FOUND'); assert.match(e.message, /not found/); assert.ok(Array.isArray(e.attempted), 'should expose attempted paths'); } }); // SC #6: env-var fallback flag > env > default test('SC #6: resolveProfile flag > env > default', () => { // flag wins const r1 = resolveProfile({ flags: { '--profile': 'balanced' } }, { VOYAGE_PROFILE: 'economy' }); assert.equal(r1.profile, 'balanced'); assert.equal(r1.profile_source, 'flag'); // env wins when no flag const r2 = resolveProfile({ flags: {} }, { VOYAGE_PROFILE: 'economy' }); assert.equal(r2.profile, 'economy'); assert.equal(r2.profile_source, 'env'); // default when neither const r3 = resolveProfile({ flags: {} }, {}); assert.equal(r3.profile, 'premium'); assert.equal(r3.profile_source, 'default'); }); // SC #7: performance — loadProfile 1000 iter < 50ms average (allowing some headroom) test('SC #7: loadProfile 1000-iter performance < 50ms average', () => { const iterations = 1000; const start = performance.now(); for (let i = 0; i < iterations; i++) { loadProfile('economy'); } const elapsed = performance.now() - start; const avgMs = elapsed / iterations; assert.ok(avgMs < 50, `loadProfile too slow: ${avgMs.toFixed(3)}ms average over ${iterations} iter`); }); // SC #8: custom.yaml from repo-root trumps ~/.claude/ test('SC #8: custom profile from /voyage-profiles/.yaml takes precedence over ~/.claude/', () => { const tmpRepo = mkdtempSync(join(tmpdir(), 'voyage-resolver-repo-')); const tmpHome = mkdtempSync(join(tmpdir(), 'voyage-resolver-home-')); try { // Place custom profile in repo and home — repo should win mkdirSync(join(tmpRepo, 'voyage-profiles'), { recursive: true }); mkdirSync(join(tmpHome, '.claude', 'voyage-profiles'), { recursive: true }); writeFileSync(join(tmpRepo, 'voyage-profiles', 'mycustom.yaml'), `--- profile_version: "1.0" name: mycustom-repo phase_models: - phase: brief model: sonnet - phase: research model: sonnet - phase: plan model: sonnet - phase: execute model: sonnet - phase: review model: sonnet - phase: continue model: sonnet parallel_agents_min: 1 parallel_agents_max: 2 external_research_enabled: false brief_reviewer_iter_cap: 1 --- `); writeFileSync(join(tmpHome, '.claude', 'voyage-profiles', 'mycustom.yaml'), `--- profile_version: "1.0" name: mycustom-home phase_models: - phase: brief model: opus - phase: research model: opus - phase: plan model: opus - phase: execute model: opus - phase: review model: opus - phase: continue model: opus parallel_agents_min: 1 parallel_agents_max: 2 external_research_enabled: true brief_reviewer_iter_cap: 3 --- `); const found = findProfilePath('mycustom', { cwd: tmpRepo, home: tmpHome }); assert.ok(found.path, `expected to find mycustom; attempted: ${found.attempted.join(', ')}`); assert.ok(found.path.startsWith(tmpRepo), `expected repo-rot win (path under ${tmpRepo}), got: ${found.path}`); const p = loadProfile('mycustom', { cwd: tmpRepo, home: tmpHome }); assert.equal(p.name, 'mycustom-repo', 'repo profile should win'); assert.equal(p.phase_models.brief, 'sonnet'); } finally { rmSync(tmpRepo, { recursive: true, force: true }); rmSync(tmpHome, { recursive: true, force: true }); } }); test('SC #8: missing profile error message includes both attempted paths', () => { const tmpRepo = mkdtempSync(join(tmpdir(), 'voyage-resolver-empty-')); const tmpHome = mkdtempSync(join(tmpdir(), 'voyage-resolver-emptyhome-')); try { try { loadProfile('not-a-real-profile', { cwd: tmpRepo, home: tmpHome }); assert.fail('expected throw'); } catch (e) { assert.equal(e.cause, 'PROFILE_NOT_FOUND'); // Both attempted paths should be in the error message for diagnostic clarity const msg = e.message; assert.match(msg, /voyage-profiles\/not-a-real-profile\.yaml/); assert.match(msg, /\.claude\/voyage-profiles\/not-a-real-profile\.yaml/); } } finally { rmSync(tmpRepo, { recursive: true, force: true }); rmSync(tmpHome, { recursive: true, force: true }); } }); // SC #9: resolveTrekcontinueProfile inheritance from plan-frontmatter test('SC #9: resolveTrekcontinueProfile inherits from plan-frontmatter (profile: balanced)', () => { const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-with-profile.md'); const r = resolveTrekcontinueProfile(planPath, { flags: {} }); assert.equal(r.profile, 'balanced'); assert.equal(r.profile_source, 'inheritance'); }); test('SC #9: resolveTrekcontinueProfile flag overrides plan-frontmatter (advisory)', () => { const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-with-profile.md'); const advisories = []; const fakeConsole = { error: (m) => advisories.push(m) }; const r = resolveTrekcontinueProfile(planPath, { flags: { '--profile': 'economy' } }, { console: fakeConsole }); assert.equal(r.profile, 'economy'); assert.equal(r.profile_source, 'flag'); assert.equal(advisories.length, 1, 'expected one advisory message'); assert.match(advisories[0], /balanced.*economy/); assert.match(advisories[0], /\[voyage\]/); }); // Backward-compat edge-case: v4.0-style plan WITHOUT profile field test('Backward-compat: resolveTrekcontinueProfile on v4.0 plan without profile field returns default premium', () => { const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-without-profile.md'); const r = resolveTrekcontinueProfile(planPath, { flags: {} }); assert.equal(r.profile, 'premium'); assert.equal(r.profile_source, 'default'); }); test('Backward-compat: resolveTrekcontinueProfile with non-existent plan path returns default premium', () => { const r = resolveTrekcontinueProfile('/tmp/does-not-exist-plan-xyz.md', { flags: {} }); assert.equal(r.profile, 'premium'); assert.equal(r.profile_source, 'default'); }); // validateProfileFile re-export sanity test('validateProfileFile re-exports validateProfile (locked-interface compat)', () => { const r = validateProfileFile(join(REPO_ROOT, 'lib', 'profiles', 'economy.yaml')); assert.equal(r.valid, true); });