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('답변 끝 <'); }); }); describe('[v2.2.310] LM Studio LSEP 합성 추론 구분자', () => { const MARKER = '__LM_STUDIO_INTERNAL_LSEP_SYNTHETIC_REASONING_END_f4e9a8d2c6b14d0c9e5f3a7b8c1d2e6a__'; /** run + separator 신호 관찰 버전. */ function runWithSignal(tokens: string[]): { out: string; replacedAt: number[] } { const f = new LiveReasoningFilter(); let out = ''; const replacedAt: number[] = []; tokens.forEach((t, i) => { const visible = f.push(t); if (f.consumeSeparatorSignal()) { out = visible; // streamReplace 시뮬레이션 — 화면 리셋 replacedAt.push(i); } else { out += visible; } }); out += f.flush(); return { out, replacedAt }; } test('END 마커 뒤 텍스트만 최종 화면에 남는다 (실사례 재현)', () => { const { out, replacedAt } = runWithSignal([ 'User message: "안녕, 잘지내?" 분석... Option 1... Final choice...', MARKER + '잘 지내고 있습니다. 별일 없으셨나요?', ]); expect(out).toBe('잘 지내고 있습니다. 별일 없으셨나요?'); expect(replacedAt).toEqual([1]); // 마커 도착 시점에 streamReplace 신호 }); test('마커가 토큰 경계에 잘게 쪼개져도 잡는다', () => { const { out } = runWithSignal([ '추론 텍스트 어쩌고 ', '__LM_STUDIO_INTERNAL_LSEP', '_SYNTHETIC_REASONING_END_f4e9a8d2c6b14d0c', '9e5f3a7b8c1d2e6a__', '실제 답변', ]); expect(out).toBe('실제 답변'); }); test('START 변형이 오면 END 까지 통째로 숨긴다', () => { const start = '__LM_STUDIO_INTERNAL_LSEP_SYNTHETIC_REASONING_START_abc123def456__'; const { out } = runWithSignal(['답변 앞부분 ', start + '숨길 추론 ' + MARKER, '답변 뒷부분']); expect(out).toBe('답변 앞부분 답변 뒷부분'); }); test('마커 없는 일반 밑줄 텍스트는 그대로 통과 (오탐 없음)', () => { const { out, replacedAt } = runWithSignal(['snake_case_variable 와 __init__ 메서드', ' 설명입니다']); expect(out).toBe('snake_case_variable 와 __init__ 메서드 설명입니다'); expect(replacedAt).toEqual([]); }); }); describe('[v2.2.310] sanitizeAssistantContent — LSEP 최종 정리 (이중 방어)', () => { const { sanitizeAssistantContent } = require('../src/lib/contextBuilders/outputSanitization'); const MARKER = '__LM_STUDIO_INTERNAL_LSEP_SYNTHETIC_REASONING_END_f4e9a8d2c6b14d0c9e5f3a7b8c1d2e6a__'; test('마커 앞 추론 전체 제거, 답변만 남긴다', () => { const raw = 'User message 분석... Option 1... Final choice...' + MARKER + '잘 지내고 있습니다. 별일 없으셨나요?'; expect(sanitizeAssistantContent(raw)).toBe('잘 지내고 있습니다. 별일 없으셨나요?'); }); test('마커가 여러 번이면 마지막 마커 뒤만', () => { const raw = '추론1' + MARKER + '중간' + MARKER + '최종 답변'; expect(sanitizeAssistantContent(raw)).toBe('최종 답변'); }); test('마커 없으면 기존 동작 그대로', () => { expect(sanitizeAssistantContent('일반 답변입니다.')).toBe('일반 답변입니다.'); }); });