/** * Research Agent / Skill Score / Success Pattern DB (Self-Evolving OS Phase 6) 테스트. */ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { parseBrief, fallbackBrief, runResearch, formatProposalMarkdown } from '../src/intelligence/researchAgent'; import { computeSkillScores, formatSkillScoresMarkdown, isSuccessTurn, appendSuccessPattern, loadSuccessPatterns, } from '../src/intelligence/skillScore'; import type { QueueItem } from '../src/intelligence/learningQueue'; import type { ReflectionRecord } from '../src/intelligence/reflectionStore'; const ITEM: QueueItem = { id: 'need-market-research', topic: '시장조사 역량 보강', priority: 60, reason: '근거 없는 수행 다수', status: 'approved', createdAt: 'a', updatedAt: 'a', }; function mk(partial: Partial): ReflectionRecord { return { ts: '2026-06-11T10:00:00.000Z', taskId: 'meeting-minutes', taskLabel: '회의록', confidenceScore: 80, confidenceBand: 'medium', missing: [], escalated: false, criticIssues: null, promptPreview: '회의록 정리해줘', usedSources: ['회의기록.md'], ...partial, }; } describe('researchAgent', () => { it('parseBrief — 잡설 섞인 JSON 파싱, 실패 시 fallback', () => { const ok = parseBrief('계획: {"questions":["q1","q2"],"keywords":["k"],"sourceTypes":["공식 문서"]} 끝'); expect(ok!.questions).toEqual(['q1', 'q2']); expect(parseBrief('JSON 없음')).toBeNull(); expect(fallbackBrief('주제').questions.length).toBeGreaterThan(0); }); it('runResearch — 브리프→내부현황→초안→Validation 게이트 (출처 없음 = review)', async () => { const calls: string[] = []; const pkg = await runResearch({ item: ITEM, fetchInternalRefs: async () => [{ title: '기존문서', content: '기존 시장조사 노트 내용', filePath: 'a.md' }], callLlm: async (system) => { calls.push(system.slice(0, 20)); if (system.includes('조사 계획')) { return '{"questions":["시장 규모 출처는?"],"keywords":["시장조사"],"sourceTypes":["공식 통계"]}'; } return '## 시장 규모\n일반적으로 통계청 자료를 쓴다 (모델 지식 — 추정, 출처 확인 필요)'; }, nowIso: '2026-06-11T00:00:00.000Z', }); expect(pkg.brief.questions[0]).toContain('시장 규모'); expect(pkg.internalRefs.length).toBe(1); expect(pkg.draft).toContain('추정'); // 출처가 없으므로 자동 수용 불가 — Permission Based Learning 게이트. expect(pkg.validation.verdict).toBe('review'); expect(pkg.validation.checks.hasSource).toBe(false); expect(calls.length).toBe(2); }); it('LLM 전부 실패해도 fallback 브리프로 패키지 생성', async () => { const pkg = await runResearch({ item: ITEM, fetchInternalRefs: async () => [], callLlm: async () => { throw new Error('down'); }, nowIso: '2026-06-11T00:00:00.000Z', }); expect(pkg.brief.questions.length).toBeGreaterThan(0); expect(pkg.draft).toContain('실패'); }); it('formatProposalMarkdown — 판정·브리프·다음 단계 포함', async () => { const pkg = await runResearch({ item: ITEM, fetchInternalRefs: async () => [], callLlm: async () => '{"questions":["q"],"keywords":["k"],"sourceTypes":["s"]}', nowIso: '2026-06-11T00:00:00.000Z', }); const md = formatProposalMarkdown(pkg, { dateStr: 'now', modelName: 'gemma' }); expect(md).toContain('검증 판정: review'); expect(md).toContain('/research'); expect(md).toContain('done 으로 변경'); }); }); describe('skillScore', () => { it('확신도·충족률·비에스컬레이션 가중 합산 + 추세', () => { const records = [ mk({ ts: '2026-06-01T10:00:00Z', confidenceScore: 50, missing: ['기한'], escalated: true }), mk({ ts: '2026-06-02T10:00:00Z', confidenceScore: 55, missing: ['기한'] }), mk({ ts: '2026-06-08T10:00:00Z', confidenceScore: 90, missing: [] }), mk({ ts: '2026-06-09T10:00:00Z', confidenceScore: 95, missing: [] }), ]; const scores = computeSkillScores(records); expect(scores.length).toBe(1); expect(scores[0].trend).toBe('up'); expect(scores[0].secondHalf).toBeGreaterThan(scores[0].firstHalf); const md = formatSkillScoresMarkdown(scores); expect(md).toContain('상승'); }); it('표본 4건 미만이면 추세 flat', () => { const scores = computeSkillScores([mk({}), mk({ confidenceScore: 20 })]); expect(scores[0].trend).toBe('flat'); }); it('isSuccessTurn — 전 요소 충족 + 확신도 90+ 만', () => { expect(isSuccessTurn(mk({ confidenceScore: 92, missing: [] }))).toBe(true); expect(isSuccessTurn(mk({ confidenceScore: 92, missing: ['기한'] }))).toBe(false); expect(isSuccessTurn(mk({ confidenceScore: 80, missing: [] }))).toBe(false); }); it('append → load 성공 패턴 라운드트립 (성공 turn 만 저장)', () => { const brain = fs.mkdtempSync(path.join(os.tmpdir(), 'astra-test-sp-')); expect(appendSuccessPattern(brain, mk({ confidenceScore: 95, missing: [] }))).toBe(true); expect(appendSuccessPattern(brain, mk({ confidenceScore: 50 }))).toBe(false); const patterns = loadSuccessPatterns(brain); expect(patterns.length).toBe(1); expect(patterns[0].usedSources).toEqual(['회의기록.md']); }); });