diff --git a/plugins/voyage/tests/lib/profile-resolver.test.mjs b/plugins/voyage/tests/lib/profile-resolver.test.mjs new file mode 100644 index 0000000..4eef940 --- /dev/null +++ b/plugins/voyage/tests/lib/profile-resolver.test.mjs @@ -0,0 +1,62 @@ +// 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'); +});