diff --git a/tests/lib/profile-application.test.mjs b/tests/lib/profile-application.test.mjs index 6a36513..5f334a5 100644 --- a/tests/lib/profile-application.test.mjs +++ b/tests/lib/profile-application.test.mjs @@ -50,6 +50,20 @@ test('SC #5: loadProfile("premium") returns all-opus', () => { } }); +test('SC #5: loadProfile("fable") returns all-fable (BUILTIN_NAMES canary)', () => { + // Throws PROFILE_NOT_FOUND if 'fable' regresses out of BUILTIN_NAMES — + // the silent-fallback-to-premium failure mode this pin exists to catch. + const p = loadProfile('fable'); + assert.equal(p.name, 'fable'); + for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) { + assert.equal(p.phase_models[phase], 'fable', `fable ${phase} should be fable`); + } + assert.equal(p.parallel_agents_min, 6); + assert.equal(p.parallel_agents_max, 8); + assert.equal(p.external_research_enabled, true); + assert.equal(p.brief_reviewer_iter_cap, 3); +}); + test('SC #5: loadProfile throws PROFILE_NOT_FOUND for unknown profile', () => { try { loadProfile('does-not-exist-xyz'); diff --git a/tests/lib/profile-resolver.test.mjs b/tests/lib/profile-resolver.test.mjs index 4eef940..f87fb42 100644 --- a/tests/lib/profile-resolver.test.mjs +++ b/tests/lib/profile-resolver.test.mjs @@ -60,3 +60,30 @@ test('resolvePhaseModel — Case 6 (defensive): null briefPath falls through to 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'); +});