/** * [v2.2.313] 파트별 완성 — 길이 한계에 닿은 보고서를 남은 섹션 단위로 나눠 생성해 * 한 문서로 통합. "더 좁은 주제로 나눠 질문하세요" 떠넘기기의 대체. */ import { completeSectioned, parseOutlineTitles, dropAlreadyWritten, } from '../src/agent/handlePrompt/sectionedCompletion'; import type { ChatMessage } from '../src/agent'; const BASE: ChatMessage[] = [ { role: 'system', content: 'SYSTEM CONTEXT (파일 근거: agent.ts, memoryContext.ts)' }, { role: 'user', content: '프로젝트 분석해줘' }, ]; describe('parseOutlineTitles / dropAlreadyWritten', () => { test('불릿·번호·헤딩 마커를 벗기고 제목만 추출', () => { const raw = '- 구조·현황\n2) 핵심 발견\n## 다음 단계\n\n없음이 아님'; expect(parseOutlineTitles(raw, 4)).toEqual(['구조·현황', '핵심 발견', '다음 단계', '없음이 아님']); }); test('"없음" 단독 응답은 빈 목록', () => { expect(parseOutlineTitles('없음', 4)).toEqual([]); }); test('초안에 이미 있는 섹션은 제외 (공백 무시 매치)', () => { const titles = ['핵심 발견', '다음 단계']; expect(dropAlreadyWritten(titles, '## 핵심 발견\n이미 썼다')).toEqual(['다음 단계']); }); }); describe('completeSectioned', () => { test('outline → 섹션별 생성 → 한 문서로 통합', async () => { const calls: string[] = []; const r = await completeSectioned({ draft: '## 개요\n잘린 초안입니다.', userPrompt: '프로젝트 분석해줘', baseMessages: BASE, requiredElements: ['구조·현황', '다음 단계'], callLLM: async (messages) => { const last = String(messages[messages.length - 1].content); calls.push(last.slice(0, 30)); if (last.includes('남은 섹션 제목')) return '구조·현황\n다음 단계'; if (last.includes('"구조·현황"')) return '## 구조·현황\nsrc/agent.ts 중심 구조.'; if (last.includes('"다음 단계"')) return '테스트 보강.'; // 헤딩 없이 와도 return ''; }, }); expect(r.sectionsAdded).toBe(2); expect(r.merged).toContain('잘린 초안입니다.'); expect(r.merged).toContain('## 구조·현황'); expect(r.merged).toContain('## 다음 단계\n테스트 보강.'); // 헤딩 자동 보강 // 섹션 호출에 시스템 컨텍스트가 재사용됐는지 (근거 접근 유지) expect(calls.length).toBe(3); // outline 1 + 섹션 2 }); test('outline 이 "없음"이면 초안 유지', async () => { const r = await completeSectioned({ draft: '완결된 초안'.repeat(10), userPrompt: 'q', baseMessages: BASE, callLLM: async () => '없음', }); expect(r.sectionsAdded).toBe(0); expect(r.merged).toContain('완결된 초안'); }); test('한 섹션 생성 실패는 건너뛰고 나머지로 통합', async () => { const r = await completeSectioned({ draft: '초안 본문입니다.', userPrompt: 'q', baseMessages: BASE, callLLM: async (messages) => { const last = String(messages[messages.length - 1].content); if (last.includes('남은 섹션 제목')) return 'A섹션\nB섹션'; if (last.includes('"A섹션"')) throw new Error('LLM down'); return '## B섹션\n본문'; }, }); expect(r.sectionsAdded).toBe(1); expect(r.merged).toContain('## B섹션'); }); test('전 섹션 실패 시 초안 그대로 (폴백)', async () => { const r = await completeSectioned({ draft: '초안', userPrompt: 'q', baseMessages: BASE, callLLM: async (messages) => { const last = String(messages[messages.length - 1].content); if (last.includes('남은 섹션 제목')) return 'A섹션'; throw new Error('down'); }, }); expect(r.sectionsAdded).toBe(0); expect(r.merged).toBe('초안'); }); });