/** * 레슨 네트워크(A-MEM 이식) + 통합 초안 형식 — 순수 로직 테스트. */ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { lessonSimilarity, addRelatedLink, linkRelatedLessons } from '../src/intelligence/lessonNetwork'; import { reconcileDraftMarkdown, type ConflictFinding } from '../src/features/growth/conflictScan'; import { tokenize } from '../src/retrieval/scoring'; const tmpBrain = () => fs.mkdtempSync(path.join(os.tmpdir(), 'astra-net-')); function mkLesson(brain: string, name: string, body: string): string { const dir = path.join(brain, 'lessons'); fs.mkdirSync(dir, { recursive: true }); const fp = path.join(dir, `${name}.md`); fs.writeFileSync(fp, body, 'utf8'); return fp; } describe('lessonSimilarity', () => { test('겹치는 토큰 비율 — 동일 주제 > 무관 주제', () => { const a = tokenize('회의 날짜를 잘못 기억해 캘린더 등록 오류 발생'); const b = tokenize('회의 일정 등록 시 날짜 확인 누락으로 오류'); const c = tokenize('파이썬 가상환경 의존성 충돌 해결 방법'); expect(lessonSimilarity(a, b)).toBeGreaterThan(lessonSimilarity(a, c)); expect(lessonSimilarity(a, a)).toBe(1); expect(lessonSimilarity([], a)).toBe(0); }); }); describe('addRelatedLink', () => { test('섹션 없으면 생성, 있으면 append, 중복은 멱등', () => { let c = '# Lesson: X\n\n## Fix\n내용\n'; c = addRelatedLink(c, '레슨A'); expect(c).toContain('## 관련 레슨\n- [[레슨A]]'); c = addRelatedLink(c, '레슨B'); expect(c).toContain('- [[레슨A]]\n- [[레슨B]]'); const again = addRelatedLink(c, '레슨A'); expect(again).toBe(c); // 멱등 }); test('관련 레슨 섹션 뒤에 다른 헤딩이 있어도 섹션 안에 삽입', () => { const c = '# L\n\n## 관련 레슨\n- [[기존]]\n\n## Applies To\n- 태그\n'; const out = addRelatedLink(c, '신규'); expect(out.indexOf('- [[신규]]')).toBeLessThan(out.indexOf('## Applies To')); }); }); describe('linkRelatedLessons — 상호 링크 + 역방향 갱신', () => { test('유사 레슨과 양방향 링크 생성', () => { const brain = tmpBrain(); const oldFp = mkLesson(brain, 'old-meeting-date', '# Lesson: 회의 날짜 오류\n\n## Mistake / Risk\n회의 날짜를 잘못 기억해 캘린더 등록 오류 발생\n'); mkLesson(brain, 'unrelated-python', '# Lesson: 파이썬 환경\n\n## Mistake / Risk\n파이썬 가상환경 의존성 충돌 npm 빌드 도구 체인 별개 주제\n'); const newFp = mkLesson(brain, 'new-meeting-date', '# Lesson: 회의 일정 재발\n\n## Mistake / Risk\n회의 일정 등록 시 날짜 확인 누락 캘린더 오류\n'); const linked = linkRelatedLessons(brain, newFp); expect(linked).toBeGreaterThanOrEqual(1); expect(fs.readFileSync(newFp, 'utf8')).toContain('[[old-meeting-date]]'); expect(fs.readFileSync(oldFp, 'utf8')).toContain('[[new-meeting-date]]'); // 역방향 }); test('유사 레슨 없으면 0 (파일 미변경)', () => { const brain = tmpBrain(); mkLesson(brain, 'a', '# Lesson: 완전히 다른 알파 베타 감마 주제\n'); const newFp = mkLesson(brain, 'b', '# Lesson: 전혀 무관한 델타 입실론 제타\n'); const before = fs.readFileSync(newFp, 'utf8'); expect(linkRelatedLessons(brain, newFp)).toBe(0); expect(fs.readFileSync(newFp, 'utf8')).toBe(before); }); }); describe('reconcileDraftMarkdown', () => { test('frontmatter + 비자동반영 고지 + 본문', () => { const f: ConflictFinding = { newDoc: 'AI_and_ML/신규.md', existingDoc: 'AI_and_ML/기존.md', summary: '버전 수치 모순', recommend: '권고: **신규 우선** (S·1.0 vs A·0.8)', }; const md = reconcileDraftMarkdown(f, '## 통합 내용\n…', '2026-06-12T00:00:00Z'); expect(md).toMatch(/^---\ntype: reconcile-draft/); expect(md).toContain('status: pending-review'); expect(md).toContain('자동 반영되지 않습니다'); expect(md).toContain('버전 수치 모순'); expect(md).toContain('신규 우선'); }); });