import { extractVisibleFinal, shouldFinalOnlyRetry, shouldAutoContinue, looksCutOff, 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.looksCutOff', () => { it('flags answers that plainly end mid-sentence / mid-structure', () => { expect(looksCutOff('당신은 복잡한 아이디어나 목표를 구체적인 실행 계획과 체계적인 문서화로')).toBe(true); // ends with the particle "로" expect(looksCutOff('우리는 이 문제를 해결하기 위해 다음과 같은 단계를')).toBe(true); // ends with object marker "를" expect(looksCutOff('the implementation is not yet complete and we need to')).toBe(true); // mid-English expect(looksCutOff('the items are: foo, bar,')).toBe(true); // trailing comma expect(looksCutOff('here is the code:\n```python\nprint("hi")')).toBe(true); // unclosed fence expect(looksCutOff('정리하면 다음 항목들이 중요합니다:\n- 첫 번째 항목\n- 두 번째 항목\n- ')).toBe(true); // dangling bullet }); it('does NOT flag complete-looking answers', () => { expect(looksCutOff('이것은 완전히 끝난 답변이고 마침표도 붙어 있습니다.')).toBe(false); expect(looksCutOff('이것은 마침표 없이 끝나는 한국어 문장입니다')).toBe(false); // ends with "다" — valid expect(looksCutOff('네, 그렇게 하면 됩니다')).toBe(false); expect(looksCutOff('done.')).toBe(false); expect(looksCutOff('짧음')).toBe(false); // too short to judge }); }); describe('responseRecovery.shouldAutoContinue', () => { it('continues when the engine reports the output cap was hit', () => { expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 3500, 4096)).toBe(true); expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 50, 4096)).toBe(true); // engine said so → trust it }); it('continues when generation reached ~the cap even if the engine said "complete"', () => { expect(shouldAutoContinue('complete', 'x'.repeat(200), 4000, 4096)).toBe(true); }); it('continues when the answer plainly ends mid-sentence (engine reason unclear)', () => { expect(shouldAutoContinue('unknown', '당신은 복잡한 아이디어나 목표를 구체적인 실행 계획과 체계적인 문서화로', 60, 4096)).toBe(true); expect(shouldAutoContinue('complete', 'the implementation continues here and we still need to', 100, 4096)).toBe(true); }); it('does NOT continue from a tiny fragment or a complete-looking answer', () => { expect(shouldAutoContinue('output-limit', 'short', 4000, 4096)).toBe(false); // < 24 chars expect(shouldAutoContinue('complete', '이것은 완전히 끝난 답변이고 마침표도 붙어 있습니다.', 100, 4096)).toBe(false); expect(shouldAutoContinue('unknown', '이것은 마침표 없이 끝나는 한국어 문장이고 충분히 길다고 본다', 100, 4096)).toBe(false); }); it('does NOT continue on stop reasons that more text cannot fix', () => { expect(shouldAutoContinue('context-overflow', 'x'.repeat(200), 4000, 4096)).toBe(false); expect(shouldAutoContinue('error', 'x'.repeat(200), 4000, 4096)).toBe(false); expect(shouldAutoContinue('user-stopped', 'x'.repeat(200), 4000, 4096)).toBe(false); expect(shouldAutoContinue('tool-calls', '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('…'); }); });