/** * Knowledge Validation / Belief Revision / Decay / Debt * (Self-Evolving OS Phase 4 — 지식 운영) 테스트. */ import { validateKnowledgeCandidate, jaccardSimilarity, ExistingKnowledgeRef, } from '../src/intelligence/knowledgeValidation'; import { auditKnowledgeDecay, classifyDecayRule, decayFactor, formatDecayReport, } from '../src/intelligence/knowledgeDecay'; import { computeKnowledgeDebt, formatNeedsMarkdown } from '../src/intelligence/needEngine'; import type { ReflectionRecord } from '../src/intelligence/reflectionStore'; const NOW = Date.parse('2026-06-11T00:00:00Z'); const DAY = 86400000; describe('knowledgeValidation', () => { const existing: ExistingKnowledgeRef[] = [ { title: 'GA4 전환율 가이드', content: 'GA4 전환율 계산은 전환수 나누기 세션수 기준이며 보고서는 탐색 분석에서 본다', lastUpdated: NOW - 100 * DAY, }, ]; it('출처 있고 중복/충돌 없는 신선한 후보 → accept', () => { const r = validateKnowledgeCandidate( { title: '쿠팡 SEO', content: '쿠팡 검색 알고리즘은 판매량과 리뷰 점수를 핵심 신호로 사용한다', source: 'https://example.com', collectedAt: '2026-06-10T00:00:00Z' }, existing, { nowMs: NOW }, ); expect(r.verdict).toBe('accept'); expect(r.beliefRevision).toBe('add'); }); it('출처 없으면 자동 수용 불가 (review)', () => { const r = validateKnowledgeCandidate( { title: 't', content: '완전히 새로운 내용의 지식 후보입니다 검증 테스트' }, existing, { nowMs: NOW }, ); expect(r.verdict).toBe('review'); expect(r.checks.hasSource).toBe(false); }); it('거의 동일한 내용 → 중복 reject', () => { const r = validateKnowledgeCandidate( { title: 'GA4', content: 'GA4 전환율 계산은 전환수 나누기 세션수 기준이며 보고서는 탐색 분석에서 본다', source: 's', collectedAt: '2026-06-10T00:00:00Z' }, existing, { nowMs: NOW }, ); expect(r.verdict).toBe('reject'); expect(r.checks.duplicateOf).toBe('GA4 전환율 가이드'); }); it('관련/충돌 + 후보가 더 최신 → review + update 권고 (Belief Revision)', () => { const r = validateKnowledgeCandidate( { title: 'GA4 변경', content: 'GA4 전환율 계산은 이제 전환수 나누기 사용자수 기준으로 변경되었다 보고서 위치도 다르다', source: 's', collectedAt: '2026-06-01T00:00:00Z' }, existing, { nowMs: NOW }, ); expect(r.verdict).toBe('review'); expect(r.checks.conflictsWith).toBe('GA4 전환율 가이드'); expect(r.beliefRevision).toBe('update'); }); it('수집일이 1년 이상 경과 → stale review', () => { const r = validateKnowledgeCandidate( { title: 't', content: '전혀 다른 주제의 오래된 지식 항목', source: 's', collectedAt: '2024-01-01T00:00:00Z' }, existing, { nowMs: NOW }, ); expect(r.verdict).toBe('review'); expect(r.checks.freshness).toBe('stale'); }); it('jaccardSimilarity — 동일 1.0, 무관 ~0', () => { expect(jaccardSimilarity('같은 문장 테스트', '같은 문장 테스트')).toBe(1); expect(jaccardSimilarity('완전히 다른 내용', '전혀 무관한 주제')).toBe(0); }); }); describe('knowledgeDecay', () => { it('분야 분류 — AI 30일, SEO 90일, 기본 365일', () => { expect(classifyDecayRule('Topics/RAG_청킹_전략.md').halfLifeDays).toBe(30); expect(classifyDecayRule('Topics/네이버_SEO_가이드.md').halfLifeDays).toBe(90); expect(classifyDecayRule('Topics/요리_레시피.md').halfLifeDays).toBe(365); }); it('decayFactor — 반감기 경과 시 0.5', () => { expect(decayFactor(NOW - 30 * DAY, 30, NOW)).toBeCloseTo(0.5, 2); expect(decayFactor(NOW, 30, NOW)).toBe(1); }); it('audit — stale 우선 정렬 + 상태 판정', () => { const items = auditKnowledgeDecay([ { relPath: 'ai_guide.md', lastUpdated: NOW - 90 * DAY }, // AI 30일 반감 → 0.125 stale { relPath: '요리.md', lastUpdated: NOW - 30 * DAY }, // 일반 365일 → ~0.94 active ], { nowMs: NOW }); expect(items[0].relPath).toBe('ai_guide.md'); expect(items[0].status).toBe('stale'); expect(items[1].status).toBe('active'); }); it('formatDecayReport — 요약·권고 포함, 자동 삭제 없음 명시', () => { const items = auditKnowledgeDecay([{ relPath: 'ai.md', lastUpdated: NOW - 200 * DAY }], { nowMs: NOW }); const md = formatDecayReport(items, { brainName: 'B', dateStr: 'now' }); expect(md).toContain('노후 1'); expect(md).toContain('자동 이동/삭제 없음'); }); }); describe('knowledgeDebt', () => { function mk(partial: Partial): ReflectionRecord { return { ts: '2026-06-11T10:00:00.000Z', taskId: 'market-research', taskLabel: '시장조사', confidenceScore: 50, confidenceBand: 'low', missing: [], escalated: false, criticIssues: null, promptPreview: 'p', weakGrounding: true, gapSeverity: 'high', ...partial, }; } it('근거 없는 수행 turn 을 업무별로 집계, debtScore 정렬', () => { const debt = computeKnowledgeDebt([ mk({}), mk({}), mk({ gapSeverity: 'medium' }), mk({ taskId: 'meeting-minutes', taskLabel: '회의록', gapSeverity: 'low' }), mk({ taskId: 'meeting-minutes', taskLabel: '회의록', weakGrounding: false }), // 부채 아님 ]); expect(debt[0].taskId).toBe('market-research'); expect(debt[0].blockedTurns).toBe(3); expect(debt[0].impact).toBeGreaterThan(5); expect(debt.find((d) => d.taskId === 'meeting-minutes')!.blockedTurns).toBe(1); }); it('formatNeedsMarkdown 에 Debt 섹션 포함', () => { const debt = computeKnowledgeDebt([mk({})]); const md = formatNeedsMarkdown([], [], debt); expect(md).toContain('Knowledge Debt'); expect(md).toContain('시장조사'); }); }); describe('orgMemoryBlock (P5)', () => { const fsMod = require('fs'); const osMod = require('os'); const pathMod = require('path'); const { buildOrgMemoryBlock, ORG_MEMORY_REL_PATH } = require('../src/intelligence/orgMemoryBlock'); it('organization.md 가 있으면 블록 주입 + Human Override 명시', () => { const brain = fsMod.mkdtempSync(pathMod.join(osMod.tmpdir(), 'astra-test-org-')); const file = pathMod.join(brain, ORG_MEMORY_REL_PATH); fsMod.mkdirSync(pathMod.dirname(file), { recursive: true }); fsMod.writeFileSync(file, '## 업무 방식\n- 속도 우선, 완벽주의 지양', 'utf8'); const block = buildOrgMemoryBlock(brain); expect(block).toContain('[ORGANIZATIONAL MEMORY]'); expect(block).toContain('속도 우선'); expect(block).toContain('사용자 지시 우선'); }); it('파일 없으면 빈 문자열 (no-op)', () => { const brain = fsMod.mkdtempSync(pathMod.join(osMod.tmpdir(), 'astra-test-org-')); expect(buildOrgMemoryBlock(brain)).toBe(''); }); it('본문이 길면 cap + 잘림 안내', () => { const brain = fsMod.mkdtempSync(pathMod.join(osMod.tmpdir(), 'astra-test-org-')); const file = pathMod.join(brain, ORG_MEMORY_REL_PATH); fsMod.mkdirSync(pathMod.dirname(file), { recursive: true }); fsMod.writeFileSync(file, 'x'.repeat(5000), 'utf8'); const block = buildOrgMemoryBlock(brain, { maxBodyLength: 1000 }); expect(block).toContain('잘림'); expect(block.length).toBeLessThan(2000); }); });