Files
connectai/tests/responseRecovery.test.ts
T

119 lines
5.9 KiB
TypeScript

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 <think>…</think> block', () => {
const out = extractVisibleFinal('<think>reasoning here, multi\nline</think>\n\n실제 답변입니다.');
expect(out.visible).toBe('실제 답변입니다.');
expect(out.hadHiddenReasoning).toBe(true);
});
it('strips an unclosed <think> running to EOS → thought-only', () => {
const out = extractVisibleFinal("<think>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('…');
});
});