import { describe, expect, it } from 'vitest'; import { resolveAgentProviderModel, resolveMovementProviderModel, resolveProviderModelCandidates, } from '../core/piece/provider-resolution.js'; describe('resolveProviderModelCandidates', () => { it('should resolve first defined provider and model independently', () => { const result = resolveProviderModelCandidates([ { provider: undefined, model: 'model-1' }, { provider: 'codex', model: undefined }, { provider: 'claude', model: 'model-2' }, ]); expect(result.provider).toBe('codex'); expect(result.model).toBe('model-1'); }); it('should return undefined fields when all candidates are undefined', () => { const result = resolveProviderModelCandidates([ {}, { provider: undefined, model: undefined }, ]); expect(result.provider).toBeUndefined(); expect(result.model).toBeUndefined(); }); }); describe('resolveMovementProviderModel', () => { it('should prefer personaProviders.provider over step.provider when both are defined', () => { const result = resolveMovementProviderModel({ step: { provider: 'codex', model: undefined, personaDisplayName: 'coder' }, provider: 'claude', personaProviders: { coder: { provider: 'opencode' } }, }); expect(result.provider).toBe('opencode'); }); it('should use personaProviders.provider when step.provider is undefined', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: undefined, personaDisplayName: 'reviewer' }, provider: 'claude', personaProviders: { reviewer: { provider: 'opencode' } }, }); expect(result.provider).toBe('opencode'); }); it('should fallback to input.provider when persona mapping is missing', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: undefined, personaDisplayName: 'unknown' }, provider: 'mock', personaProviders: { reviewer: { provider: 'codex' } }, }); expect(result.provider).toBe('mock'); }); it('should return undefined provider when all provider candidates are missing', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: undefined, personaDisplayName: 'none' }, provider: undefined, personaProviders: undefined, }); expect(result.provider).toBeUndefined(); }); it('should prefer personaProviders.model over step.model and input.model', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: 'step-model', personaDisplayName: 'coder' }, model: 'input-model', personaProviders: { coder: { provider: 'codex', model: 'persona-model' } }, }); expect(result.model).toBe('persona-model'); }); it('should use personaProviders.model when step.model is undefined', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: undefined, personaDisplayName: 'coder' }, model: 'input-model', personaProviders: { coder: { provider: 'codex', model: 'persona-model' } }, }); expect(result.model).toBe('persona-model'); }); it('should fallback to input.model when step.model and personaProviders.model are undefined', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: undefined, personaDisplayName: 'coder' }, model: 'input-model', personaProviders: { coder: { provider: 'codex' } }, }); expect(result.model).toBe('input-model'); }); it('should return undefined model when all model candidates are missing', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: undefined, personaDisplayName: 'coder' }, model: undefined, personaProviders: { coder: { provider: 'codex' } }, }); expect(result.model).toBeUndefined(); }); it('should resolve provider from personaProviders entry with only model specified', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: undefined, personaDisplayName: 'coder' }, provider: 'claude', personaProviders: { coder: { model: 'o3-mini' } }, }); expect(result.provider).toBe('claude'); expect(result.model).toBe('o3-mini'); }); it('should resolve cursor provider from personaProviders', () => { const result = resolveMovementProviderModel({ step: { provider: undefined, model: undefined, personaDisplayName: 'coder' }, provider: 'claude', personaProviders: { coder: { provider: 'cursor' } }, }); expect(result.provider).toBe('cursor'); }); }); describe('resolveAgentProviderModel', () => { it.each([ { name: 'CLI overrides every other layer and also overrides model', input: { cliProvider: 'codex' as const, cliModel: 'cli-model', personaProviders: { coder: { provider: 'mock' as const, model: 'persona-model' }, }, personaDisplayName: 'coder', stepProvider: 'claude' as const, stepModel: 'step-model', localProvider: 'opencode' as const, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'codex' as const, model: 'cli-model' }, }, { name: 'Step overrides local/global when persona is missing', input: { stepProvider: 'claude' as const, stepModel: 'step-model', localProvider: 'opencode' as const, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'claude' as const, model: 'step-model' }, }, { name: 'Persona provider wins when CLI is absent', input: { stepProvider: 'claude' as const, personaProviders: { coder: { provider: 'mock' as const }, }, personaDisplayName: 'coder', localProvider: 'opencode' as const, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'mock' as const, model: 'global-model' }, }, { name: 'Persona model wins when no step model and no CLI model', input: { stepProvider: 'claude' as const, stepModel: undefined, personaProviders: { coder: { model: 'persona-only-model' }, }, personaDisplayName: 'coder', localProvider: 'opencode' as const, localModel: 'local-model', globalProvider: 'claude' as const, globalModel: 'global-model', }, expected: { provider: 'claude' as const, model: 'persona-only-model' }, }, { name: 'Step provider wins over local/global provider and step model wins over model-only candidates', input: { stepProvider: 'codex' as const, stepModel: 'step-model', personaProviders: { coder: { model: 'persona-model' }, }, personaDisplayName: 'coder', localProvider: 'claude' as const, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'codex' as const, model: 'persona-model' }, }, { name: 'Local provider is used when no higher-priority provider exists', input: { localProvider: 'opencode' as const, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'opencode' as const, model: 'local-model' }, }, { name: 'Global is used when local provider is absent', input: { globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'mock' as const, model: 'global-model' }, }, { name: 'No CLI provider or higher layer, CLI model still has model-layer priority', input: { cliModel: 'cli-model', stepModel: 'step-model', localProvider: undefined, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'mock' as const, model: 'cli-model' }, }, { name: 'All providers absent, earliest defined model in model order is used', input: { stepModel: 'step-model', localProvider: undefined, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'mock' as const, model: 'step-model' }, }, { name: 'Local model is ignored when it does not match resolved provider', input: { stepProvider: 'opencode' as const, localProvider: 'codex' as const, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'opencode' as const, model: undefined }, }, { name: 'Global model is used when it matches resolved provider', input: { stepProvider: 'claude' as const, localProvider: 'opencode' as const, localModel: 'local-model', globalProvider: 'claude' as const, globalModel: 'global-model', }, expected: { provider: 'claude' as const, model: 'global-model' }, }, { name: 'Local model is preferred when both local and global providers match', input: { localProvider: 'mock' as const, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'mock' as const, model: 'local-model' }, }, { name: 'Global model is used when local exists but does not match resolved provider', input: { stepProvider: 'codex' as const, localProvider: 'opencode' as const, localModel: 'local-model', globalProvider: 'codex' as const, globalModel: 'global-model', }, expected: { provider: 'codex' as const, model: 'global-model' }, }, { name: 'CLI model is used even when provider comes from local and no CLI provider', input: { cliModel: 'cli-model', localProvider: 'mock' as const, localModel: 'local-model', globalProvider: 'mock' as const, globalModel: 'global-model', }, expected: { provider: 'mock' as const, model: 'cli-model' }, }, { name: 'Persona provider resolves provider, persona model still takes model priority', input: { stepProvider: 'codex' as const, stepModel: 'step-model', personaProviders: { coder: { provider: 'mock' as const, model: 'persona-model', }, }, personaDisplayName: 'coder', localProvider: 'claude' as const, localModel: 'local-model', globalProvider: 'opencode' as const, globalModel: 'global-model', }, expected: { provider: 'mock' as const, model: 'persona-model' }, }, { name: 'Unknown persona name falls back to normal chain without persona model/provider', input: { stepProvider: 'claude' as const, stepModel: 'step-model', personaProviders: { reviewer: { provider: 'mock' as const, model: 'persona-model' }, }, personaDisplayName: 'coder', localProvider: 'mock' as const, localModel: 'local-model', }, expected: { provider: 'claude' as const, model: 'step-model' }, }, { name: 'No providers defined and no models defined -> all undefined', input: {}, expected: { provider: undefined, model: undefined }, }, { name: 'Only CLI model with persona-only model (no provider match), model remains persona-first', input: { cliModel: 'cli-model', personaProviders: { coder: { model: 'persona-model' }, }, personaDisplayName: 'coder', stepProvider: 'mock' as const, stepModel: 'step-model', }, expected: { provider: 'mock' as const, model: 'cli-model' }, }, ])('should resolve %s', ({ input, expected }) => { const result = resolveAgentProviderModel(input); expect(result).toEqual(expected); }); it('should resolve provider in order: CLI > persona > movement > local > global', () => { const result = resolveAgentProviderModel({ cliProvider: 'opencode', stepProvider: 'claude', localProvider: 'codex', globalProvider: 'claude', personaProviders: { coder: { provider: 'mock' } }, personaDisplayName: 'coder', }); expect(result.provider).toBe('opencode'); }); it('should use persona override when no CLI provider is set', () => { const result = resolveAgentProviderModel({ stepProvider: 'claude', localProvider: 'codex', globalProvider: 'claude', personaProviders: { coder: { provider: 'opencode', model: 'persona-model' } }, personaDisplayName: 'coder', }); expect(result.provider).toBe('opencode'); expect(result.model).toBe('persona-model'); }); it('should fall back to movement provider when persona override is not configured', () => { const result = resolveAgentProviderModel({ stepProvider: 'claude', localProvider: 'codex', globalProvider: 'claude', personaProviders: { reviewer: { provider: 'mock', model: 'o3-mini' } }, personaDisplayName: 'coder', }); expect(result.provider).toBe('claude'); }); it('should prefer local config provider/model over global config for same provider', () => { const result = resolveAgentProviderModel({ localProvider: 'codex', localModel: 'local-model', globalProvider: 'codex', globalModel: 'global-model', }); expect(result.provider).toBe('codex'); expect(result.model).toBe('local-model'); }); it('should prefer global config when local config is not set', () => { const result = resolveAgentProviderModel({ localProvider: undefined, globalProvider: 'claude', globalModel: 'global-model', }); expect(result.provider).toBe('claude'); expect(result.model).toBe('global-model'); }); it('should resolve model order: CLI > persona > movement > config candidate matching provider', () => { const result = resolveAgentProviderModel({ cliModel: 'cli-model', stepModel: 'movement-model', localProvider: 'claude', localModel: 'local-model', globalProvider: 'codex', globalModel: 'global-model', cliProvider: 'codex', personaProviders: { coder: { model: 'persona-model' } }, personaDisplayName: 'coder', }); expect(result.provider).toBe('codex'); expect(result.model).toBe('cli-model'); }); it('should use movement model when persona model is absent', () => { const result = resolveAgentProviderModel({ stepModel: 'movement-model', localProvider: 'claude', localModel: 'local-model', globalProvider: 'codex', globalModel: 'global-model', personaProviders: { coder: { provider: 'opencode' } }, personaDisplayName: 'coder', }); expect(result.provider).toBe('opencode'); expect(result.model).toBe('movement-model'); }); it('should apply local/ global model only when provider matches resolved provider', () => { const result = resolveAgentProviderModel({ localProvider: 'claude', localModel: 'local-model', globalProvider: 'codex', globalModel: 'global-model', stepProvider: 'codex', }); expect(result.provider).toBe('codex'); expect(result.model).toBe('global-model'); }); it('should ignore local and global model when provider does not match', () => { const result = resolveAgentProviderModel({ localProvider: 'codex', localModel: 'local-model', globalProvider: 'claude', globalModel: 'global-model', stepProvider: 'opencode', }); expect(result.provider).toBe('opencode'); expect(result.model).toBeUndefined(); }); it('should combine persona and movement overrides in one run', () => { const result = resolveAgentProviderModel({ cliProvider: 'codex', stepProvider: 'claude', stepModel: 'movement-model', localProvider: 'claude', localModel: 'local-model', globalProvider: 'mock', globalModel: 'global-model', cliModel: 'cli-model', personaProviders: { coder: { provider: 'mock', model: 'persona-model', }, }, personaDisplayName: 'coder', }); expect(result.provider).toBe('codex'); expect(result.model).toBe('cli-model'); }); it('should apply full priority chain when all layers are present', () => { const result = resolveAgentProviderModel({ cliProvider: 'codex', cliModel: 'cli-model', personaProviders: { reviewer: { provider: 'mock', model: 'persona-model', }, }, personaDisplayName: 'reviewer', stepProvider: 'claude', stepModel: 'step-model', localProvider: 'opencode', localModel: 'local-model', globalProvider: 'claude', globalModel: 'global-model', }); expect(result.provider).toBe('codex'); expect(result.model).toBe('cli-model'); }); it('should apply full priority chain without cli overrides', () => { const result = resolveAgentProviderModel({ personaProviders: { reviewer: { provider: 'mock', model: 'persona-model', }, }, personaDisplayName: 'reviewer', stepProvider: 'claude', stepModel: 'step-model', localProvider: 'opencode', localModel: 'local-model', globalProvider: 'claude', globalModel: 'global-model', }); expect(result.provider).toBe('mock'); expect(result.model).toBe('persona-model'); }); it('should keep model and provider priorities consistent for fallback path', () => { const result = resolveAgentProviderModel({ stepProvider: 'claude', localProvider: 'codex', localModel: 'local-model', globalProvider: 'claude', globalModel: 'global-model', }); expect(result.provider).toBe('claude'); expect(result.model).toBe('global-model'); }); it('should keep model fallback after persona-only model when step model is absent', () => { const result = resolveAgentProviderModel({ personaProviders: { reviewer: { model: 'persona-model', }, }, personaDisplayName: 'reviewer', stepProvider: 'claude', localProvider: 'codex', localModel: 'local-model', globalProvider: 'codex', globalModel: 'global-model', }); expect(result.provider).toBe('claude'); expect(result.model).toBe('persona-model'); }); });