/** * [v2.2.314] 두 실사례 가드: * 1) LSEP 마커 뒤 "마무리 인사"만 남고 본문이 추론 구간에 갇혀 증발한 답변. * 2) "요청 요약/사용자 의도 추론/핵심 확인 질문" 내부 스캐폴딩 섹션 유출. */ import { splitLsepReasoning, isCloserOnlyAnswer, looksLikeReportBody, stripLeakedInternalSections, } from '../src/lib/contextBuilders/outputSanitization'; const MARKER = '__LM_STUDIO_INTERNAL_LSEP_SYNTHETIC_REASONING_END_a1b2c3d4e5f6__'; describe('splitLsepReasoning / isCloserOnlyAnswer / looksLikeReportBody', () => { test('마지막 END 마커 기준으로 before/after 분해', () => { const body = '## 1. 구조\n분석 본문. '.repeat(50); const r = splitLsepReasoning(`${body}${MARKER}이상으로 분석을 마칩니다.`); expect(r.hasMarker).toBe(true); expect(r.before).toContain('## 1. 구조'); expect(r.after).toBe('이상으로 분석을 마칩니다.'); }); test('마커 없으면 hasMarker=false, after=원문', () => { const r = splitLsepReasoning('일반 답변'); expect(r.hasMarker).toBe(false); expect(r.after).toBe('일반 답변'); }); test('마무리 인사만 = closer-only, 실질 본문은 아님', () => { expect(isCloserOnlyAnswer('이상으로 ConnectAI 프로젝트에 대한 분석을 마칩니다. 추가 검토가 필요하시면 언제든 요청해 주세요.')).toBe(true); expect(isCloserOnlyAnswer('## 구조 분석\n실제 본문이 여기 길게 이어진다.')).toBe(false); }); test('헤딩 다수 장문 = 보고 본문, 짧은 텍스트 = 아님', () => { const report = ['## 개요', '본문 문단. '.repeat(60), '## 구조', '본문 문단. '.repeat(60)].join('\n\n'); expect(looksLikeReportBody(report)).toBe(true); expect(looksLikeReportBody('짧은 생각')).toBe(false); }); }); describe('stripLeakedInternalSections — 내부 섹션 유출 제거', () => { test('금지 4종 섹션과 "질문 의도:" 줄을 제거하고 본문·출처는 보존', () => { const answer = [ '## 프로젝트 개요', '실제 분석 본문.', '## 요청 요약', 'ConnectAI 프로젝트 분석 요청입니다.', '## 사용자 의도 추론', '전체 그림을 이해하고자 하는 의도로 판단됩니다.', '## 프로젝트 기록 대상 확인', '기록으로 관리될 예정입니다.', '## 핵심 확인 질문', '심층 분석이 필요하신가요?', '질문 의도: 범위 확장 여부 결정.', '## 분석 결과 요약', '요약 본문.', ].join('\n'); const out = stripLeakedInternalSections(answer); expect(out).toContain('## 프로젝트 개요'); expect(out).toContain('## 분석 결과 요약'); expect(out).not.toContain('요청 요약'); expect(out).not.toContain('의도 추론'); expect(out).not.toContain('기록 대상 확인'); expect(out).not.toContain('핵심 확인 질문'); expect(out).not.toContain('질문 의도'); }); test('금지 섹션이 없으면 원문 유지', () => { const clean = '## 개요\n본문\n\n## 다음 단계\n제안'; expect(stripLeakedInternalSections(clean)).toBe(clean); }); });