// packages/core/__tests__/personal-graph.test.ts // 개인 그래프 순수 함수 테스트 — 용어 추출/문장 분해/관계/순위. import { describe, expect, it } from 'vitest' import { SHARES_TERMS_MIN_SIMILARITY, buildSequenceEdges, continuationsFrom, extractTerms, jaccard, rankRelated, splitSentences } from '../src/personal-graph' describe('extractTerms', () => { it('소문자화하고 조사/불용어를 걷어낸다', () => { const terms = extractTerms('오늘 회의에서 논의한 내용을 정리해서 공유드립니다') expect(terms).toContain('회의에서') expect(terms).toContain('정리해서') expect(terms).not.toContain('그리고') }) it('영어 불용어를 제거한다', () => { const terms = extractTerms('Thanks for the quick turnaround on the report') expect(terms).toContain('quick') expect(terms).toContain('turnaround') expect(terms).toContain('report') expect(terms).not.toContain('the') }) it('빈도 순으로 상위 limit 개만 돌려준다', () => { const terms = extractTerms('배포 배포 배포 점검 점검 테스트', 2) expect(terms).toEqual(['배포', '점검']) }) it('1자 토큰은 버린다', () => { expect(extractTerms('a b c 배포')).toEqual(['배포']) }) }) describe('splitSentences', () => { it('종결 부호와 개행으로 나누고 짧은 조각은 버린다', () => { const sentences = splitSentences('오늘 회의는 여기서 끝. 짧음. 내일 일정을 공유드릴게요!') expect(sentences).toContain('오늘 회의는 여기서 끝') expect(sentences).toContain('내일 일정을 공유드릴게요') expect(sentences).not.toContain('짧음') }) }) describe('buildSequenceEdges', () => { it('인접한 문장 쌍만 만든다', () => { const edges = buildSequenceEdges(['A 문장입니다', 'B 문장입니다', 'C 문장입니다']) expect(edges).toEqual([ { from: 'A 문장입니다', to: 'B 문장입니다' }, { from: 'B 문장입니다', to: 'C 문장입니다' } ]) }) it('같은 문장이 반복되면 만들지 않는다', () => { expect(buildSequenceEdges(['같은 문장입니다', '같은 문장입니다'])).toEqual([]) }) }) describe('jaccard', () => { it('겹침 비율을 계산한다', () => { expect(jaccard(['a', 'b'], ['a', 'b'])).toBe(1) expect(jaccard(['a', 'b'], ['c', 'd'])).toBe(0) expect(jaccard(['a', 'b', 'c'], ['a', 'b', 'd'])).toBeCloseTo(0.5, 5) }) it('빈 집합은 0 이다', () => { expect(jaccard([], ['a'])).toBe(0) }) }) describe('continuationsFrom', () => { it('꼬리 뒤에 실제로 이어 쓴 텍스트만 뽑는다', () => { const out = continuationsFrom( ['오늘 회의에서 논의한 내용을 정리해서 공유드립니다', '관계 없는 문장입니다'], '논의한 내용을' ) expect(out).toEqual(['정리해서 공유드립니다']) }) it('꼬리가 너무 짧으면 아무것도 만들지 않는다', () => { expect(continuationsFrom(['아무 문장'], '아무')).toEqual([]) }) it('길이를 제한하고 중복을 없앤다', () => { const out = continuationsFrom( ['접두 뒤에 이어지는 아주 긴 문장이 계속 이어집니다 그리고 더 이어집니다', '접두 뒤에 이어지는 아주 긴 문장이 계속 이어집니다'], '접두 뒤에', 2, 20 ) expect(out).toHaveLength(1) expect(out[0].length).toBeLessThanOrEqual(20) }) }) describe('rankRelated', () => { it('관계 가중치와 용어 유사도, 최근성을 함께 본다', () => { const now = Date.now() const ranked = rankRelated( ['회의', '일정'], [ { text: '회의 일정을 공유드립니다', terms: ['회의', '일정'], weight: 0, lastUsedAt: now }, { text: '전혀 상관 없는 문장입니다', terms: ['바다', '산'], weight: 0, lastUsedAt: now }, { text: '강하게 이어지는 문장입니다', terms: [], weight: 10, lastUsedAt: null } ], 3 ) // 관련도 순: 강한 엣지 → 용어 유사 + 최근성 → 무관(마지막) expect(ranked[0]).toBe('강하게 이어지는 문장입니다') expect(ranked[1]).toBe('회의 일정을 공유드립니다') expect(ranked[2]).toBe('전혀 상관 없는 문장입니다') }) it('용어 유사도 임계값 상수는 0과 1 사이이다', () => { expect(SHARES_TERMS_MIN_SIMILARITY).toBeGreaterThan(0) expect(SHARES_TERMS_MIN_SIMILARITY).toBeLessThan(1) }) })