Files
connectai/tests/reportQa.test.ts
T
koriweb 47b3b9f93a v2.2.299~307: 아키텍처 수렴 + 채팅 정리 + 벤치마킹 강화 + Claude 구독 엔진
- v2.2.299 아키텍처 수렴: coreChat 통일(엔진 휴리스틱 3벌 제거), 기업 모드
  검색 오케스트레이터 승격, lib/execUtil(실행 래퍼 6곳·Python 탐지 3벌 단일화),
  lib/kstSchedule(워처 4개 nowInKst 통합), estimateTokens 통합, 설정 접근 규칙 명문화
- v2.2.300 채팅 화면 정리: LiveReasoningFilter(스트리밍 중 <think>/Harmony 추론
  토큰 단위 차단), 확신도·검토요청 footer 기본 숨김(계산·Reflection 은 유지)
- v2.2.301 문맥·의도 이해: [답변 전 이해 원칙] 상시 주입, 워크플로우 의도 브리핑,
  Report QA 루프(규칙 레지스트리+실측치+회귀 게이트, 블로그_v3 개념 이식)
- v2.2.302 /benchmark 비즈니스 렌즈(가격·수익·운영)+빌드 프롬프트 모드+QA 연계
- v2.2.303 handoff 모드(측정치 무손실 인수인계 문서)+/claude(Claude Code 터미널 위임)
- v2.2.304 이식성: 지식 경로 두뇌-상대 규약(pickWikiDir 상대 해석), 이사 체크리스트
- v2.2.305 Claude 구독 엔진: claude: 프로바이더(CLI 위임, 모델 드롭다운 자동 노출,
  coreChat 지원 — 워크플로우·QA도 구독 모델 가능)
- v2.2.306 Tone Guard: AI 상투어 금지 레지스트리(상담사 화법 실사례 8종+대조 예시)
- v2.2.307 /benchmark 레이아웃 골격(sectionRoles 결정론 분석, 롤링 배너 즉답),
  파트별 실패 격리, 합성 타임아웃 120→300초

검증: tsc 무오류 + jest 888 통과 + esbuild 정상

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 21:02:15 +09:00

116 lines
6.2 KiB
TypeScript

import { measureReport, parseLooseJson, runReportQaLoop, REPORT_QA_RULES, buildReportQaItems, reportQaCriticalNums, formatIntentBrief } from '../src/agents/reportQa';
jest.mock('../src/core/services', () => ({
coreChat: jest.fn(),
}));
const { coreChat } = require('../src/core/services');
describe('measureReport — 결정적 실측치', () => {
test('본문 글자 수는 마크다운 문법을 제외하고 센다', () => {
const m = measureReport('## 제목\n\n본문 다섯글자\n\n```js\ncode();\n```');
expect(m.contentChars).toBe('제목 본문 다섯글자'.length);
expect(m.sectionCount).toBe(1);
});
test('출처 인용 흔적과 나열 비율을 측정한다', () => {
const text = [
'## 발견', '결론입니다 (출처: 문서A)', '「위키 문서B」에 따르면 그렇다.',
'- 항목 1', '- 항목 2', '일반 지식 기반의 추정이다.',
].join('\n');
const m = measureReport(text);
expect(m.citationCount).toBeGreaterThanOrEqual(3);
expect(m.listLineRatio).toBeGreaterThan(0);
});
});
describe('parseLooseJson — 관대한 JSON 파싱', () => {
test('코드펜스에 싸인 JSON 을 파싱한다', () => {
expect(parseLooseJson('```json\n{"score": 85, "feedback": "좋음"}\n```')).toEqual({ score: 85, feedback: '좋음' });
});
test('앞뒤 잡담이 섞여도 첫 { ~ 마지막 } 로 복구한다', () => {
expect(parseLooseJson('알겠습니다. {"score": 70} 입니다.')).toEqual({ score: 70 });
});
test('복구 불가면 null', () => {
expect(parseLooseJson('점수는 85점입니다')).toBeNull();
});
});
describe('QA 규칙 레지스트리', () => {
test('번호가 고유하고 채점 항목 문자열이 전 규칙을 포함한다', () => {
const nums = REPORT_QA_RULES.map(r => r.num);
expect(new Set(nums).size).toBe(nums.length);
const items = buildReportQaItems();
for (const r of REPORT_QA_RULES) expect(items).toContain(`(${r.num})`);
});
test('중대 결함 번호 목록은 critical 규칙에서 자동 생성된다', () => {
const criticals = REPORT_QA_RULES.filter(r => r.critical).map(r => r.num);
expect(reportQaCriticalNums()).toBe(criticals.join('·'));
});
});
describe('runReportQaLoop — 채점·재작성·회귀 게이트', () => {
beforeEach(() => (coreChat as jest.Mock).mockReset());
const brief = { purpose: 'p', realQuestions: ['q'], expectedUse: 'u' };
const longDraft = '초안 본문 '.repeat(50);
test('임계 이상이면 재작성 없이 그대로 통과', async () => {
(coreChat as jest.Mock).mockResolvedValueOnce({ content: '{"score": 88, "feedback": "통과"}', empty: false });
const r = await runReportQaLoop({ draft: longDraft, prompt: 'X 조사해줘', intentBrief: brief });
expect(r.finalDraft).toBe(longDraft);
expect(r.score).toBe(88);
expect(r.revisions).toBe(0);
});
test('임계 미달 → 재작성 → 점수 상승 시 채택', async () => {
const revised = '개선된 보고서 본문 '.repeat(40);
(coreChat as jest.Mock)
.mockResolvedValueOnce({ content: '{"score": 70, "feedback": "3번 근거 부족"}', empty: false }) // 초안 채점
.mockResolvedValueOnce({ content: revised, empty: false }) // 재작성
.mockResolvedValueOnce({ content: '{"score": 86, "feedback": "통과"}', empty: false }); // 재채점
const r = await runReportQaLoop({ draft: longDraft, prompt: 'X 조사해줘', intentBrief: brief });
expect(r.finalDraft).toBe(revised.trim());
expect(r.score).toBe(86);
expect(r.revisions).toBe(1);
expect(r.trajectory).toEqual([70, 86]);
});
test('[회귀 게이트] 재작성본 점수가 떨어지면 폐기하고 최고 초안 유지', async () => {
const worse = '나빠진 보고서 본문 '.repeat(40);
(coreChat as jest.Mock)
.mockResolvedValueOnce({ content: '{"score": 75, "feedback": "1번 미흡"}', empty: false })
.mockResolvedValueOnce({ content: worse, empty: false })
.mockResolvedValueOnce({ content: '{"score": 68, "feedback": "더 나빠짐"}', empty: false })
.mockResolvedValueOnce({ content: worse, empty: false })
.mockResolvedValueOnce({ content: '{"score": 60, "feedback": "악화"}', empty: false });
const r = await runReportQaLoop({ draft: longDraft, prompt: 'X 조사해줘', intentBrief: brief });
expect(r.finalDraft).toBe(longDraft); // 원본 유지
expect(r.score).toBe(75);
expect(r.trajectory).toEqual([75, 68, 60]);
});
test('채점 자체가 실패(JSON 파싱 불가)하면 초안을 그대로 반환한다', async () => {
(coreChat as jest.Mock).mockResolvedValueOnce({ content: '점수를 매길 수 없습니다', empty: false });
const r = await runReportQaLoop({ draft: longDraft, prompt: 'X 조사해줘', intentBrief: null });
expect(r.finalDraft).toBe(longDraft);
expect(r.score).toBeNull();
});
test('재작성본이 과도하게 짧으면(잘림) 회귀로 간주해 폐기한다', async () => {
(coreChat as jest.Mock)
.mockResolvedValueOnce({ content: '{"score": 60, "feedback": "부족"}', empty: false })
.mockResolvedValueOnce({ content: '짧음', empty: false }) // 심하게 잘린 재작성
.mockResolvedValueOnce({ content: '짧음', empty: false })
const r = await runReportQaLoop({ draft: longDraft, prompt: 'X 조사해줘', intentBrief: brief });
expect(r.finalDraft).toBe(longDraft);
});
});
describe('formatIntentBrief', () => {
test('브리핑을 성공 기준 지시와 함께 포맷한다', () => {
const out = formatIntentBrief({ purpose: '시장 진입 판단', realQuestions: ['경쟁 강도', '규제'], expectedUse: '투자 결정' });
expect(out).toContain('시장 진입 판단');
expect(out).toContain('경쟁 강도 / 규제');
expect(out).toContain('표면 질문만 문자적으로 답하지 마라');
});
});