// packages/core/__tests__/voice-llm-route.test.ts // 받아쓰기 LLM 경로 결정 우선순위 표 테스트 (override > chain > instruction > action > none). import { describe, expect, it } from 'vitest' import { resolveVoiceLlmRoute, routeUsesLlm, type VoiceLlmRouteInput } from '../src/voice-llm-route' const base: VoiceLlmRouteInput = { defaultAction: 'refine', overrideInstructionId: null, activeChainId: null, activeInstructionId: null, localLlmAvailable: true, backend: 'local', } function route(patch: Partial): ReturnType { return resolveVoiceLlmRoute({ ...base, ...patch }) } describe('resolveVoiceLlmRoute — 우선순위 표', () => { it.each([ // [설명, 입력, 기대 경로] ['override 는 체인보다 앞선다', { defaultAction: 'chain', activeChainId: 'c1', overrideInstructionId: 'i9' }, { kind: 'instruction', instructionId: 'i9', source: 'override' }], ['override 는 none 을 이긴다', { defaultAction: 'none', overrideInstructionId: 'i9' }, { kind: 'instruction', instructionId: 'i9', source: 'override' }], ['override 는 일반 액션을 이긴다', { defaultAction: 'translate', overrideInstructionId: 'i9' }, { kind: 'instruction', instructionId: 'i9', source: 'override' }], ['override 는 활성 지시문보다 앞선다', { defaultAction: 'custom', activeInstructionId: 'i1', overrideInstructionId: 'i9' }, { kind: 'instruction', instructionId: 'i9', source: 'override' }], ['활성 체인이 있으면 체인', { defaultAction: 'chain', activeChainId: 'c1' }, { kind: 'chain', chainId: 'c1' }], ['체인인데 활성 체인이 없으면 일반 액션 chain (기존 동작)', { defaultAction: 'chain', activeChainId: null }, { kind: 'action', action: 'chain' }], ['custom 은 활성 지시문', { defaultAction: 'custom', activeInstructionId: 'i1' }, { kind: 'instruction', instructionId: 'i1', source: 'active' }], ['custom 인데 활성 지시문이 없으면 지시문 없이 custom', { defaultAction: 'custom', activeInstructionId: '' }, { kind: 'instruction', instructionId: null, source: 'active' }], ['활성 지시문은 일반 액션을 덮어쓰지 않는다', { defaultAction: 'refine', activeInstructionId: 'i1' }, { kind: 'action', action: 'refine' }], ['translate 는 일반 액션', { defaultAction: 'translate' }, { kind: 'action', action: 'translate' }], ['none 은 원문 삽입', { defaultAction: 'none' }, { kind: 'insert-raw', reason: 'none' }], ] as const)('%s', (_label, patch, expected) => { expect(route(patch as Partial)).toEqual(expected) }) }) describe('resolveVoiceLlmRoute — 로컬 LLM 가용성', () => { it('로컬 백엔드에서 LLM 을 쓸 수 없으면 사유를 밝혀 원문 삽입', () => { expect(route({ localLlmAvailable: false })).toEqual({ kind: 'insert-raw', reason: 'llm-unavailable' }) expect(route({ localLlmAvailable: false, overrideInstructionId: 'i9' })).toEqual({ kind: 'insert-raw', reason: 'llm-unavailable', }) expect(route({ localLlmAvailable: false, defaultAction: 'chain', activeChainId: 'c1' })).toEqual({ kind: 'insert-raw', reason: 'llm-unavailable', }) }) it("none 은 LLM 가용성과 무관하게 'none' 사유다 (경고 없음)", () => { expect(route({ defaultAction: 'none', localLlmAvailable: false })).toEqual({ kind: 'insert-raw', reason: 'none' }) }) it('온라인 백엔드는 로컬 가용성을 보지 않는다', () => { expect(route({ backend: 'online', localLlmAvailable: false })).toEqual({ kind: 'action', action: 'refine' }) expect(route({ backend: undefined, localLlmAvailable: false })).toEqual({ kind: 'action', action: 'refine' }) }) }) describe('routeUsesLlm', () => { it('원문 삽입만 LLM 을 쓰지 않는다', () => { expect(routeUsesLlm({ kind: 'insert-raw', reason: 'none' })).toBe(false) expect(routeUsesLlm({ kind: 'chain', chainId: 'c' })).toBe(true) expect(routeUsesLlm({ kind: 'action', action: 'refine' })).toBe(true) }) })