import { flattenClaudePrompt, buildClaudeCliArgs, extractFromStreamJson, listClaudeCodeModels } from '../src/features/providers/claudeCode'; import { parseModelPrefix, makeModelId } from '../src/features/providers/types'; describe('claude: 프리픽스 라우팅', () => { test('claude:sonnet → claude-code 프로바이더로 라우팅', () => { expect(parseModelPrefix('claude:sonnet')).toEqual({ provider: 'claude-code', model: 'sonnet' }); }); test('makeModelId 는 프리픽스 테이블을 따른다 (claude-code → claude:)', () => { expect(makeModelId('claude-code', 'sonnet')).toBe('claude:sonnet'); expect(makeModelId('anthropic', 'claude-opus-4-8')).toBe('anthropic:claude-opus-4-8'); }); test('로컬 모델은 여전히 null (로컬 엔진 경로)', () => { expect(parseModelPrefix('gemma4:e2b')).toBeNull(); }); }); describe('flattenClaudePrompt — 메시지 평탄화', () => { test('시스템 블록·화자 라벨·도구 금지 지시를 포함한다', () => { const p = flattenClaudePrompt([ { role: 'system', content: '너는 아스트라다.' }, { role: 'user', content: '안녕' }, { role: 'assistant', content: '반갑습니다' }, { role: 'user', content: '이어서 질문' }, ]); expect(p).toContain('\n너는 아스트라다.\n'); expect(p).toContain('도구를 사용하지 말고'); expect(p).toContain('[사용자]\n안녕'); expect(p).toContain('[아스트라]\n반갑습니다'); expect(p.indexOf('[사용자]\n안녕')).toBeLessThan(p.indexOf('[사용자]\n이어서 질문')); }); test('시스템 없는 단일 user 도 동작', () => { expect(flattenClaudePrompt([{ role: 'user', content: 'hi' }])).toBe('[사용자]\nhi'); }); }); describe('buildClaudeCliArgs', () => { test('headless stream-json + 모델 지정', () => { expect(buildClaudeCliArgs('sonnet')).toEqual(['-p', '--output-format', 'stream-json', '--verbose', '--model', 'sonnet']); }); test('모델 비면 --model 생략 (CLI 기본 모델)', () => { expect(buildClaudeCliArgs('')).toEqual(['-p', '--output-format', 'stream-json', '--verbose']); }); }); describe('extractFromStreamJson — CLI 출력 파싱', () => { test('assistant 이벤트의 text 블록을 이어붙인다', () => { const lines = [ '{"type":"system","subtype":"init"}', '{"type":"assistant","message":{"content":[{"type":"text","text":"안녕"},{"type":"text","text":"하세요"}]}}', '{"type":"result","subtype":"success","result":"안녕하세요"}', ]; expect(extractFromStreamJson(lines)).toEqual({ text: '안녕하세요', error: '' }); }); test('assistant 이벤트가 없으면 result 본문을 사용 (버전 차이 대응)', () => { const lines = ['{"type":"result","subtype":"success","result":"본문"}']; expect(extractFromStreamJson(lines).text).toBe('본문'); }); test('에러 result 는 error 로 추출', () => { const lines = ['{"type":"result","subtype":"error_during_execution","is_error":true,"result":"Invalid API key"}']; const r = extractFromStreamJson(lines); expect(r.text).toBe(''); expect(r.error).toContain('Invalid API key'); }); test('JSON 아닌 라인·깨진 라인은 무시', () => { expect(extractFromStreamJson(['잡음', '{broken', ''])).toEqual({ text: '', error: '' }); }); }); describe('listClaudeCodeModels', () => { test('구독 별칭 3종', () => { expect(listClaudeCodeModels()).toEqual(['sonnet', 'opus', 'haiku']); }); });