d3ro-voice/packages/core/__tests__/voice-llm-route.test.ts

68 lines
4 KiB
TypeScript

// 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<VoiceLlmRouteInput>): ReturnType<typeof resolveVoiceLlmRoute> {
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<VoiceLlmRouteInput>)).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)
})
})