import { extractVisibleFinal, shouldFinalOnlyRetry, shouldAutoContinue, mergeContinuationParts, buildContinuationUserPrompt, } from '../src/core/responseRecovery'; describe('responseRecovery.extractVisibleFinal — thought quarantine', () => { it('leaves a plain answer untouched', () => { const out = extractVisibleFinal('안녕하세요! 무엇을 도와드릴까요?'); expect(out.visible).toBe('안녕하세요! 무엇을 도와드릴까요?'); expect(out.hadHiddenReasoning).toBe(false); expect(out.wasThoughtOnly).toBe(false); }); it('keeps only the Harmony `final` channel and discards analysis', () => { const raw = '<|channel|>analysis<|message|>Let me think about this carefully...<|end|><|start|>assistant<|channel|>final<|message|>최종 답변입니다.'; const out = extractVisibleFinal(raw); expect(out.visible).toBe('최종 답변입니다.'); expect(out.hadFinalChannel).toBe(true); expect(out.hadHiddenReasoning).toBe(true); expect(out.hiddenReasoning).toContain('think about this'); expect(out.wasThoughtOnly).toBe(false); }); it('strips an UNCLOSED thought channel (model ran out of tokens mid-thought) → thought-only', () => { const raw = '<|channel>thought\nThinking Process:\nLet me figure out how to approach this and'; const out = extractVisibleFinal(raw); expect(out.visible).toBe(''); expect(out.hadHiddenReasoning).toBe(true); expect(out.wasThoughtOnly).toBe(true); expect(shouldFinalOnlyRetry(out)).toBe(true); }); it('strips a closed block', () => { const out = extractVisibleFinal('reasoning here, multi\nline\n\n실제 답변입니다.'); expect(out.visible).toBe('실제 답변입니다.'); expect(out.hadHiddenReasoning).toBe(true); }); it('strips an unclosed running to EOS → thought-only', () => { const out = extractVisibleFinal("I'm thinking and then I run out of"); expect(out.visible).toBe(''); expect(out.wasThoughtOnly).toBe(true); }); it('strips a leading "Thinking Process:" block up to the answer boundary', () => { const out = extractVisibleFinal('Thinking Process:\nStep 1: consider X\nStep 2: consider Y\n## 요약\n실제 답변 본문입니다.'); expect(out.visible).toContain('## 요약'); expect(out.visible).toContain('실제 답변 본문'); expect(out.visible).not.toContain('Step 1'); expect(out.hadHiddenReasoning).toBe(true); }); it('treats a leading "Thinking Process:" with no answer boundary as thought-only', () => { const out = extractVisibleFinal('Thinking Process:\nStep 1...\nStep 2... and I ran out of tokens here'); expect(out.visible).toBe(''); expect(out.wasThoughtOnly).toBe(true); }); it('does NOT strip a legitimate "## Thinking Process" markdown heading (no colon)', () => { const out = extractVisibleFinal('## Thinking Process\n여기서는 사고 과정 자체를 설명하는 답변입니다.'); expect(out.visible).toContain('## Thinking Process'); expect(out.visible).toContain('사고 과정 자체를 설명'); expect(out.hadHiddenReasoning).toBe(false); }); it('handles empty / whitespace input', () => { expect(extractVisibleFinal('').visible).toBe(''); expect(extractVisibleFinal(' \n ').visible).toBe(''); expect(extractVisibleFinal(null as any).visible).toBe(''); expect(extractVisibleFinal('').wasThoughtOnly).toBe(false); }); }); describe('responseRecovery.shouldAutoContinue', () => { it('continues only when output-limit AND a real visible answer AND near the cap', () => { expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 3500, 4096)).toBe(true); expect(shouldAutoContinue('output-limit', 'short', 4000, 4096)).toBe(false); // no real answer expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 100, 4096)).toBe(false); // didn't actually hit the cap expect(shouldAutoContinue('complete', 'x'.repeat(200), 4000, 4096)).toBe(false); expect(shouldAutoContinue('context-overflow', 'x'.repeat(200), 4000, 4096)).toBe(false); expect(shouldAutoContinue('error', 'x'.repeat(200), 4000, 4096)).toBe(false); }); }); describe('responseRecovery.mergeContinuationParts', () => { it('handles empty inputs', () => { expect(mergeContinuationParts('', 'hello')).toBe('hello'); expect(mergeContinuationParts('hello', '')).toBe('hello'); expect(mergeContinuationParts('', '')).toBe(''); }); it('joins with a paragraph break when the previous part ended cleanly', () => { expect(mergeContinuationParts('첫 번째 부분.', '두 번째 부분.')).toBe('첫 번째 부분.\n\n두 번째 부분.'); }); it('removes a verbatim overlap the continuation re-stated, splicing mid-sentence', () => { const a = 'the answer continues here and here'; const b = 'continues here and here, then more'; expect(mergeContinuationParts(a, b)).toBe('the answer continues here and here, then more'); }); }); describe('responseRecovery.buildContinuationUserPrompt', () => { it('includes the original question and the tail of the answer so far', () => { const p = buildContinuationUserPrompt('원래 질문은 무엇인가?', 'a'.repeat(50) + 'TAIL_MARKER'); expect(p).toContain('원래 질문은 무엇인가?'); expect(p).toContain('TAIL_MARKER'); expect(p).toMatch(/continue/i); }); it('truncates a long answer-so-far to its tail', () => { const long = 'HEAD_MARKER' + 'b'.repeat(3000) + 'TAIL_MARKER'; const p = buildContinuationUserPrompt('q', long, 1400); expect(p).toContain('TAIL_MARKER'); expect(p).not.toContain('HEAD_MARKER'); expect(p).toContain('…'); }); });