import { LiveReasoningFilter } from '../src/lib/contextBuilders/liveReasoningFilter'; /** 토큰 배열을 순서대로 밀어넣고 화면에 방출된 텍스트를 합쳐 반환. */ function run(tokens: string[]): string { const f = new LiveReasoningFilter(); let out = ''; for (const t of tokens) out += f.push(t); out += f.flush(); return out; } describe('LiveReasoningFilter — 스트리밍 중 추론 구간 실시간 차단', () => { test('일반 텍스트는 그대로 통과한다', () => { expect(run(['안녕', '하세요. ', '무엇을 도와드릴까요?'])).toBe('안녕하세요. 무엇을 도와드릴까요?'); }); test(' 블록은 통째로 숨긴다', () => { expect(run(['사용자가 인사했다', '반갑습니다.'])).toBe('반갑습니다.'); }); test('토큰 경계에 걸쳐 쪼개진 도 잡는다', () => { expect(run(['내부 ', '추론답', '변입니다'])).toBe('답변입니다'); }); test(' 변형도 처리한다', () => { expect(run(['...', 'A'])).toBe('A'); expect(run(['...', 'B'])).toBe('B'); }); test('본문 중간의 think 블록도 앞뒤 본문을 보존한다', () => { expect(run(['결론은 X입니다. ', '검산', '근거는 Y입니다.'])) .toBe('결론은 X입니다. 근거는 Y입니다.'); }); test('Harmony thought 채널은 숨기고 final 채널 본문만 통과한다', () => { const tokens = [ '<|channel|>thought The user said "안녕". This is a greeting.', '<|channel|>final<|message|>', '반갑습니다. 무엇을 도와드릴까요?', ]; expect(run(tokens)).toBe('반갑습니다. 무엇을 도와드릴까요?'); }); test('채널 마커가 토큰 경계에 걸쳐도 잡는다', () => { const tokens = ['<|chan', 'nel|>thought 내부 추론 텍스트 ', '<|channel|>fin', 'al<|mess', 'age|>답변']; expect(run(tokens)).toBe('답변'); }); test('thought 채널이 <|end|> 로 끝나면 이후는 visible', () => { expect(run(['<|channel|>thought 추론<|end|>', '본문'])).toBe('본문'); }); test('닫히지 않은 think 는 아무것도 방출하지 않는다 (최종 streamReplace 가 정리)', () => { expect(run(['끝나지 않는 추론...'])).toBe(''); }); test('마커가 아닌 < 문자는 보류 후 정상 방출된다', () => { expect(run(['a < b 이고 ', 'c > d 입니다'])).toBe('a < b 이고 c > d 입니다'); expect(run(['제네릭 Array 타입'])).toBe('제네릭 Array 타입'); }); test('스트림이 잠재 마커 접두사로 끝나면 flush 로 회수된다', () => { expect(run(['답변 끝 <'])).toBe('답변 끝 <'); }); });