// tests/lib/profile-resolver.test.mjs // v5.1.1 SC5 — non-interference cases for resolvePhaseModel(). // Verifies the new highest-priority lookup step (brief.phase_signals[phase].model) // wins over --profile flag and VOYAGE_PROFILE env; falls through cleanly when // no brief signal is present. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { resolvePhaseModel } from '../../lib/profiles/resolver.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(__dirname, '..', '..'); const FIXTURE = (name) => join(REPO_ROOT, 'tests', 'fixtures', name); test('resolvePhaseModel — Case 1: brief signal wins over VOYAGE_PROFILE env', () => { // brief-effort-low.md pins all 4 phases to model: sonnet. // env says premium (would normally select opus). Brief must win. const r = resolvePhaseModel('research', FIXTURE('brief-effort-low.md'), [], { VOYAGE_PROFILE: 'premium' }); assert.equal(r.model, 'sonnet', `brief signal should beat env; got ${JSON.stringify(r)}`); assert.equal(r.source, 'brief-signal'); }); test('resolvePhaseModel — Case 2: brief signal wins over --profile flag', () => { // brief-effort-high.md pins all 4 phases to model: opus. // flag says economy (would normally select sonnet). Brief must win. const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), ['--profile', 'economy'], {}); assert.equal(r.model, 'opus', `brief signal should beat flag; got ${JSON.stringify(r)}`); assert.equal(r.source, 'brief-signal'); }); test('resolvePhaseModel — Case 3: no phase_signals → fallthrough to --profile flag', () => { // brief-without-phase-signals fixture lacks phase_signals entirely. // --profile balanced is set. Should return balanced.phase_models.plan (= opus per yaml). const r = resolvePhaseModel('plan', FIXTURE('brief-without-phase-signals.md'), ['--profile', 'balanced'], {}); assert.equal(r.model, 'opus', `balanced.plan should be opus; got ${JSON.stringify(r)}`); assert.equal(r.source, 'flag'); }); test('resolvePhaseModel — Case 4: phase not in PHASE_SIGNAL_PHASES falls through gracefully', () => { // brief-effort-high.md has signals for the 4 supported phases. // Asking for 'continue' (not in PHASE_SIGNAL_PHASES) must fall through. // --profile premium is set, so continue resolves to premium.phase_models.continue (= opus). const r = resolvePhaseModel('continue', FIXTURE('brief-effort-high.md'), ['--profile', 'premium'], {}); assert.equal(r.model, 'opus', `premium.continue should be opus; got ${JSON.stringify(r)}`); assert.ok(r.source !== 'brief-signal', 'continue must not resolve via brief-signal'); }); test('resolvePhaseModel — Case 5 (defensive): missing brief file falls through cleanly', () => { // Non-existent path. Must not throw; must fall through to flag/env/default. const r = resolvePhaseModel('plan', '/nonexistent/brief.md', ['--profile', 'economy'], {}); assert.equal(r.model, 'sonnet', 'economy.plan should be sonnet on fallthrough'); assert.equal(r.source, 'flag'); }); test('resolvePhaseModel — Case 6 (defensive): null briefPath falls through to default', () => { // null briefPath, no flag, no env → default = premium. const r = resolvePhaseModel('plan', null, [], {}); assert.equal(r.model, 'opus', 'premium.plan default = opus'); assert.equal(r.source, 'default'); }); // v5.9 — fable profile composition (brief SC 4) + effort passthrough coherence test('resolvePhaseModel — --profile fable: all six phases resolve model fable (no brief signal)', () => { for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) { const r = resolvePhaseModel(phase, null, ['--profile', 'fable'], {}); assert.equal(r.model, 'fable', `fable.${phase} should be fable; got ${JSON.stringify(r)}`); assert.equal(r.source, 'flag'); } }); test('resolvePhaseModel — brief signal (opus) beats --profile fable (composition precedence pin)', () => { // brief-effort-high.md pins execute to model: opus. Brief must beat the fable profile. const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), ['--profile', 'fable'], {}); assert.equal(r.model, 'opus', `brief signal should beat fable profile; got ${JSON.stringify(r)}`); assert.equal(r.source, 'brief-signal'); }); test('resolvePhaseModel — composed output carries {effort, model} atomically (v5.9 passthrough)', () => { // brief-effort-high.md: execute → {effort: high, model: opus}. The composed // resolver must return BOTH fields from one call — the split-CLI design this // passthrough replaced could drift effort and model apart. const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), [], {}); assert.equal(r.effort, 'high', `effort must pass through; got ${JSON.stringify(r)}`); assert.equal(r.model, 'opus'); assert.equal(r.source, 'brief-signal'); });