Files
connectai/tests/sectionedCompletion.test.ts
koriweb c79eda1862 v2.2.313~319: 보고 품질·조사 신뢰성 수술 + 네이버 쇼핑인사이트/검색어트렌드 연동
- 파트별 완성, 웹 실접속 증빙, 턴 의도 브리핑, 본문 불일치 검증, 하위 질문
  체크리스트, 사이트 심층 수집 (PATCHNOTES.md v2.2.313~318 참조)
- /naver-shopping, /naver-trend: 네이버 데이터랩 API 연동. compare/discover
  자동 판별, API 배치 상한 캡슐화, 카테고리 트리 fuzzy 매칭, 자격증명은
  Astra 설정 패널 SecretStorage 로 관리
2026-08-05 18:20:32 +09:00

98 lines
4.3 KiB
TypeScript

/**
* [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('초안');
});
});