Skip to main content
Glama
zettelkasten-methodology.test.ts23.9 kB
/** * Zettelkasten 방법론 통합 테스트 * 원자적 노트, 양방향 링크, 백링크, 고아 노트 관리 */ import { executeTool } from '../../src/tools'; import { createTestContext, cleanupTestContext } from '../test-helpers'; import type { ToolExecutionContext } from '../../src/tools/types'; import { IndexSearchEngine } from '@inchankang/zettel-memory-index-search'; describe('Zettelkasten Methodology Integration Tests', () => { let context: ToolExecutionContext; let searchEngine: IndexSearchEngine; beforeEach(() => { context = createTestContext(); searchEngine = new IndexSearchEngine({ dbPath: context.indexPath, }); }); afterEach(() => { searchEngine.close(); cleanupTestContext(context); }); describe('UID 시스템', () => { it('모든 노트에 고유한 UID가 생성된다', async () => { const uids: string[] = []; // 여러 노트를 빠르게 생성 for (let i = 0; i < 10; i++) { const result = await executeTool( 'create_note', { title: `노트 ${i}`, content: `내용 ${i}` }, context ); uids.push(result._meta?.metadata?.id); } // 모든 UID가 고유한지 확인 const uniqueUids = new Set(uids); expect(uniqueUids.size).toBe(10); // UID 형식 검증 (YYYYMMDDTHHMMSSmmmmmmZ) for (const uid of uids) { expect(uid).toMatch(/^\d{8}T\d{12}Z$/); } }); it('UID는 시간순으로 정렬 가능하다', async () => { const notes: { uid: string; title: string }[] = []; // 시간 간격을 두고 생성 for (let i = 0; i < 3; i++) { const result = await executeTool( 'create_note', { title: `노트 ${i}`, content: `내용 ${i}` }, context ); notes.push({ uid: result._meta?.metadata?.id, title: `노트 ${i}`, }); // 짧은 대기 await new Promise((resolve) => setTimeout(resolve, 10)); } // UID로 정렬하면 생성 순서대로 정렬됨 const sortedByUid = [...notes].sort((a, b) => a.uid.localeCompare(b.uid) ); expect(sortedByUid[0].title).toBe('노트 0'); expect(sortedByUid[1].title).toBe('노트 1'); expect(sortedByUid[2].title).toBe('노트 2'); }); }); describe('원자적 노트 (Atomic Notes)', () => { it('하나의 노트는 하나의 개념을 다룬다', async () => { // 잘못된 방식: 여러 개념이 혼합 const badNote = await executeTool( 'create_note', { title: '개발 생각들', content: '1. TDD는 좋다\n2. 마이크로서비스는 복잡하다\n3. 코드 리뷰는 필수다', tags: ['dev', 'mixed'], }, context ); // 올바른 방식: 원자적 노트로 분리 const tddNote = await executeTool( 'create_note', { title: 'TDD가 코드 품질을 향상시키는 이유', content: '테스트를 먼저 작성하면:\n- 요구사항을 명확히 이해\n- 작은 단위로 설계\n- 리팩토링 안전망 제공', tags: ['tdd', 'testing', 'quality'], }, context ); const microservicesNote = await executeTool( 'create_note', { title: '마이크로서비스의 복잡성 요인', content: '마이크로서비스의 복잡성:\n- 분산 시스템 문제\n- 네트워크 지연\n- 데이터 일관성', tags: ['microservices', 'architecture', 'complexity'], }, context ); const codeReviewNote = await executeTool( 'create_note', { title: '코드 리뷰의 필수적인 이유', content: '코드 리뷰가 필수인 이유:\n- 버그 조기 발견\n- 지식 공유\n- 코드 품질 향상', tags: ['code-review', 'quality', 'collaboration'], }, context ); // 원자적 노트들을 연결 await executeTool( 'update_note', { uid: tddNote._meta?.metadata?.id, links: [codeReviewNote._meta?.metadata?.id], // TDD와 코드 리뷰 연결 }, context ); // 검색 시 더 정확한 결과 const tddSearch = await executeTool( 'search_memory', { query: 'TDD 테스트 품질' }, context ); expect(tddSearch.content[0]?.text).toContain('TDD'); const microservicesSearch = await executeTool( 'search_memory', { query: '마이크로서비스 복잡성' }, context ); expect(microservicesSearch.content[0]?.text).toContain('마이크로서비스'); }); }); describe('양방향 링크 및 백링크', () => { it('노트 간 양방향 연결을 추적할 수 있다', async () => { // MOC (Map of Content) 허브 노트 const hubNote = await executeTool( 'create_note', { title: '소프트웨어 아키텍처 MOC', content: '# 소프트웨어 아키텍처 지식 맵', tags: ['moc', 'architecture', 'hub'], }, context ); const hubUid = hubNote._meta?.metadata?.id; // 관련 개념 노트들 const solidNote = await executeTool( 'create_note', { title: 'SOLID 원칙', content: '객체지향 설계의 5가지 원칙', tags: ['solid', 'oop', 'design-principles'], links: [hubUid], }, context ); const solidUid = solidNote._meta?.metadata?.id; const cleanArchNote = await executeTool( 'create_note', { title: '클린 아키텍처', content: '의존성 역전을 통한 계층 분리', tags: ['clean-architecture', 'layers', 'dependency'], links: [hubUid, solidUid], }, context ); const cleanArchUid = cleanArchNote._meta?.metadata?.id; const dddNote = await executeTool( 'create_note', { title: '도메인 주도 설계', content: '비즈니스 도메인 중심의 설계 방법론', tags: ['ddd', 'domain', 'modeling'], links: [hubUid, cleanArchUid], }, context ); const dddUid = dddNote._meta?.metadata?.id; // 허브 노트 업데이트 await executeTool( 'update_note', { uid: hubUid, content: '# 소프트웨어 아키텍처 지식 맵\n\n## 연결된 개념\n- SOLID 원칙\n- 클린 아키텍처\n- 도메인 주도 설계', links: [solidUid, cleanArchUid, dddUid], }, context ); // 백링크 확인 const hubWithLinks = await executeTool( 'read_note', { uid: hubUid, includeLinks: true }, context ); const hubMetadata = hubWithLinks._meta?.metadata as any; expect(hubMetadata?.links?.length).toBe(3); // 백링크는 링크 그래프 인덱싱이 필요하므로 여기서는 front matter links만 확인 // Clean Architecture 노트의 연결 확인 const cleanArchWithLinks = await executeTool( 'read_note', { uid: cleanArchUid }, context ); const cleanArchMetadata = cleanArchWithLinks._meta?.metadata as any; expect(cleanArchMetadata?.links).toContain(hubUid); expect(cleanArchMetadata?.links).toContain(solidUid); // dddNote가 cleanArchUid를 참조하고 있지만, 백링크 추적은 인덱싱 후에 가능 }); it('끊어진 링크를 감지할 수 있다', async () => { // 연결된 노트들 생성 const noteA = await executeTool( 'create_note', { title: '개념 A', content: '기본 개념', tags: ['concept'], }, context ); const uidA = noteA._meta?.metadata?.id; const noteB = await executeTool( 'create_note', { title: '개념 B', content: 'A를 기반으로 확장', tags: ['concept'], links: [uidA], }, context ); const uidB = noteB._meta?.metadata?.id; const noteC = await executeTool( 'create_note', { title: '개념 C', content: 'A와 B를 종합', tags: ['concept'], links: [uidA, uidB], }, context ); const uidC = noteC._meta?.metadata?.id; // 노트 A 삭제 await executeTool('delete_note', { uid: uidA, confirm: true }, context); // 노트 B의 링크 확인 const noteBLinks = await executeTool( 'read_note', { uid: uidB, includeLinks: true }, context ); const noteBMetadata = noteBLinks._meta?.metadata as any; // 끊어진 링크가 감지되어야 함 if (noteBMetadata?.links?.broken) { expect(noteBMetadata.links.broken).toContain(uidA); } // 노트 C의 링크 확인 const noteCLinks = await executeTool( 'read_note', { uid: uidC, includeLinks: true }, context ); const noteCMetadata = noteCLinks._meta?.metadata as any; if (noteCMetadata?.links?.broken) { expect(noteCMetadata.links.broken).toContain(uidA); // uidB는 여전히 존재 expect(noteCMetadata.links.outbound).toContain(uidB); } }); }); describe('노트 네트워크 탐색', () => { it('연결된 노트 그래프를 탐색할 수 있다', async () => { // 중심 노트 const rootNote = await executeTool( 'create_note', { title: '루트 노트', content: '네트워크의 시작점', tags: ['root'], }, context ); const rootUid = rootNote._meta?.metadata?.id; // 레벨 1 노트들 const level1Notes = []; for (let i = 0; i < 3; i++) { const note = await executeTool( 'create_note', { title: `레벨 1 노트 ${i}`, content: `루트와 연결된 ${i}번 노트`, tags: ['level1'], links: [rootUid], }, context ); level1Notes.push(note._meta?.metadata?.id); } // 루트 노트에 레벨 1 연결 await executeTool( 'update_note', { uid: rootUid, links: level1Notes, }, context ); // 레벨 2 노트 (레벨 1의 첫 번째 노트에 연결) const level2Note = await executeTool( 'create_note', { title: '레벨 2 노트', content: '레벨 1에 연결', tags: ['level2'], links: [level1Notes[0]], }, context ); // 루트에서 연결된 노트 확인 const rootWithLinks = await executeTool( 'read_note', { uid: rootUid, includeLinks: true }, context ); const rootMetadata = rootWithLinks._meta?.metadata as any; expect(rootMetadata?.links?.length).toBe(3); // 백링크 추적은 링크 그래프 인덱싱이 필요 // 레벨 1 첫 번째 노트 확인 (레벨 2가 레벨 1을 참조) const level1WithLinks = await executeTool( 'read_note', { uid: level1Notes[0] }, context ); const level1Metadata = level1WithLinks._meta?.metadata as any; // level1은 rootUid를 참조함 expect(level1Metadata?.links).toContain(rootUid); }); it('순환 링크가 있어도 안정적으로 동작한다', async () => { // A → B → C → A 순환 구조 const noteA = await executeTool( 'create_note', { title: '노트 A', content: 'A', tags: ['circular'], }, context ); const uidA = noteA._meta?.metadata?.id; const noteB = await executeTool( 'create_note', { title: '노트 B', content: 'B', tags: ['circular'], links: [uidA], }, context ); const uidB = noteB._meta?.metadata?.id; const noteC = await executeTool( 'create_note', { title: '노트 C', content: 'C', tags: ['circular'], links: [uidB], }, context ); const uidC = noteC._meta?.metadata?.id; // 순환 완성: A → C await executeTool( 'update_note', { uid: uidA, links: [uidC], }, context ); // 순환 링크가 있어도 읽기 성공 const noteALinks = await executeTool( 'read_note', { uid: uidA, includeLinks: true }, context ); expect(noteALinks.content[0]?.text).toContain('노트 A'); const aMetadata = noteALinks._meta?.metadata as any; expect(aMetadata?.links).toContain(uidC); // noteB가 noteA를 참조하지만, 백링크 추적은 인덱싱 필요 // 태그로 필터링 테스트 (검색 인덱스 없음) const circularNotes = await executeTool( 'list_notes', { tags: ['circular'] }, context ); expect(circularNotes.content[0]?.text).toContain('노트'); }); }); describe('Zettelkasten 워크플로우', () => { it('점진적 정교화를 통해 지식을 구축할 수 있다', async () => { // Layer 1: 원본 자료 캡처 const originalNote = await executeTool( 'create_note', { title: '강의 노트: 분산 시스템 기초', content: `# 분산 시스템 강의 ## CAP 정리 분산 시스템은 Consistency, Availability, Partition Tolerance 중 최대 2개만 보장 ## 일관성 모델 - Strong Consistency - Eventual Consistency - Causal Consistency`, category: 'Resources', tags: ['distributed-systems', 'lecture', 'raw'], }, context ); const originalUid = originalNote._meta?.metadata?.id; // Layer 2: 핵심 요약 const summaryNote = await executeTool( 'create_note', { title: '핵심: 분산 시스템 설계 선택', content: `## CAP Trade-off **CP 선택**: 일관성 우선 (금융 시스템) **AP 선택**: 가용성 우선 (소셜 미디어)`, category: 'Resources', tags: ['distributed-systems', 'summary'], links: [originalUid], }, context ); const summaryUid = summaryNote._meta?.metadata?.id; // Layer 3: 실행 가능한 통찰 const insightNote = await executeTool( 'create_note', { title: '통찰: 프로젝트의 CAP 선택 가이드', content: `## 액션 아이템 1. 트랜잭션 서비스: CP 필수 2. 피드 서비스: AP 가능 3. 세션: Causal 충분`, category: 'Projects', project: 'system-architecture', tags: ['architecture', 'decision', 'actionable'], links: [summaryUid, originalUid], }, context ); const insightUid = insightNote._meta?.metadata?.id; // 지식 레이어별 검색 const rawNotes = await executeTool( 'list_notes', { tags: ['raw'] }, context ); expect(rawNotes._meta?.metadata?.total).toBe(1); const summaryNotes = await executeTool( 'list_notes', { tags: ['summary'] }, context ); expect(summaryNotes._meta?.metadata?.total).toBe(1); const actionableNotes = await executeTool( 'list_notes', { tags: ['actionable'] }, context ); expect(actionableNotes._meta?.metadata?.total).toBe(1); // 통찰 노트의 링크 확인 const insightLinks = await executeTool( 'read_note', { uid: insightUid, includeLinks: true }, context ); const insightMetadata = insightLinks._meta?.metadata as any; expect(insightMetadata?.links).toContain(summaryUid); expect(insightMetadata?.links).toContain(originalUid); }); it('연관 검색을 통해 관련 노트를 찾을 수 있다', async () => { // React 관련 노트들 생성 const reactBasics = await executeTool( 'create_note', { title: 'React 기초', content: 'React는 UI를 구축하기 위한 JavaScript 라이브러리입니다.', tags: ['react', 'javascript', 'frontend'], }, context ); const reactHooks = await executeTool( 'create_note', { title: 'React Hooks 가이드', content: 'useState, useEffect, useContext 등의 훅을 사용하여 함수형 컴포넌트를 작성합니다.', tags: ['react', 'hooks', 'state-management'], links: [reactBasics._meta?.metadata?.id], }, context ); const reactPerf = await executeTool( 'create_note', { title: 'React 성능 최적화', content: 'useMemo, useCallback, React.memo를 사용하여 불필요한 리렌더링을 방지합니다.', tags: ['react', 'performance', 'optimization'], links: [ reactBasics._meta?.metadata?.id, reactHooks._meta?.metadata?.id, ], }, context ); // React 검색 const reactSearch = await executeTool( 'search_memory', { query: 'React' }, context ); const searchText = reactSearch.content[0]?.text || ''; expect(searchText.toLowerCase()).toContain('react'); // Hooks 태그로 필터링 (검색 인덱스 없음) const hooksNotes = await executeTool( 'list_notes', { tags: ['hooks'] }, context ); expect(hooksNotes.content[0]?.text).toContain('React Hooks'); // 태그로 필터링 const perfNotes = await executeTool( 'list_notes', { tags: ['performance'] }, context ); expect(perfNotes.content[0]?.text).toContain('성능 최적화'); }); }); describe('고급 Zettelkasten 패턴', () => { it('허브 노트 (MOC)를 통해 지식을 구조화할 수 있다', async () => { // 프로그래밍 언어 MOC const progLangMoc = await executeTool( 'create_note', { title: '프로그래밍 언어 MOC', content: '# 프로그래밍 언어 지식 맵\n\n## 패러다임별 분류\n- 함수형\n- 객체지향\n- 절차형', tags: ['moc', 'programming-languages'], }, context ); const mocUid = progLangMoc._meta?.metadata?.id; // 하위 노트들 const jsNote = await executeTool( 'create_note', { title: 'JavaScript', content: '동적 타입, 프로토타입 기반 객체지향', tags: ['javascript', 'dynamic-typing'], links: [mocUid], }, context ); const tsNote = await executeTool( 'create_note', { title: 'TypeScript', content: '정적 타입을 추가한 JavaScript 슈퍼셋', tags: ['typescript', 'static-typing'], links: [mocUid, jsNote._meta?.metadata?.id], }, context ); const pythonNote = await executeTool( 'create_note', { title: 'Python', content: '동적 타입, 다중 패러다임 언어', tags: ['python', 'dynamic-typing'], links: [mocUid], }, context ); // MOC 업데이트 await executeTool( 'update_note', { uid: mocUid, content: '# 프로그래밍 언어 지식 맵\n\n## 연결된 언어\n- JavaScript\n- TypeScript\n- Python', links: [ jsNote._meta?.metadata?.id, tsNote._meta?.metadata?.id, pythonNote._meta?.metadata?.id, ], }, context ); // MOC의 연결 확인 const mocLinks = await executeTool( 'read_note', { uid: mocUid, includeLinks: true }, context ); const mocMetadata = mocLinks._meta?.metadata as any; expect(mocMetadata?.links?.length).toBe(3); // 백링크는 인덱싱이 필요하므로 front matter links만 확인 // TypeScript의 다중 연결 확인 const tsLinks = await executeTool( 'read_note', { uid: tsNote._meta?.metadata?.id }, context ); const tsMetadata = tsLinks._meta?.metadata as any; expect(tsMetadata?.links).toContain(mocUid); expect(tsMetadata?.links).toContain( jsNote._meta?.metadata?.id ); }); it('문헌 노트와 영구 노트를 구분할 수 있다', async () => { // 문헌 노트 (Literature Notes) - 외부 소스에서 가져온 정보 const literatureNote = await executeTool( 'create_note', { title: '도서: Clean Code by Robert C. Martin', content: `## 주요 내용 - 의미 있는 이름 사용 - 함수는 한 가지 일만 해야 함 - 주석보다 코드로 의도를 표현 - 오류 처리를 위한 try-catch ## 출처 Clean Code: A Handbook of Agile Software Craftsmanship, Chapter 1-4`, category: 'Resources', tags: ['literature-note', 'book', 'clean-code'], }, context ); const litUid = literatureNote._meta?.metadata?.id; // 영구 노트 (Permanent Notes) - 내 생각을 담은 노트 const permanentNote1 = await executeTool( 'create_note', { title: '좋은 함수명이 주석을 대체하는 이유', content: `함수명이 명확하면: - 코드 자체가 문서가 됨 - 주석과 코드의 불일치 문제 해결 - 검색 가능성 향상 예시: - Bad: // 사용자 확인 후 이메일 전송 sendEmail(); - Good: validateUserAndSendWelcomeEmail();`, tags: ['permanent-note', 'naming', 'clean-code'], links: [litUid], }, context ); const permanentNote2 = await executeTool( 'create_note', { title: '단일 책임 원칙과 함수 크기의 관계', content: `SOLID의 SRP를 함수 레벨에 적용: - 한 함수 = 한 가지 책임 - 자연스럽게 작은 함수가 됨 - 테스트하기 쉬워짐`, tags: ['permanent-note', 'solid', 'clean-code'], links: [litUid], }, context ); // 문헌 노트와 영구 노트 분류 const literatureNotes = await executeTool( 'list_notes', { tags: ['literature-note'] }, context ); expect(literatureNotes._meta?.metadata?.total).toBe(1); const permanentNotes = await executeTool( 'list_notes', { tags: ['permanent-note'] }, context ); expect(permanentNotes._meta?.metadata?.total).toBe(2); // 문헌 노트에서 파생된 영구 노트 확인 (permanent notes가 literature note를 참조) const litWithLinks = await executeTool( 'read_note', { uid: litUid }, context ); const litMetadata = litWithLinks._meta?.metadata as any; // 문헌 노트 자체는 다른 노트를 참조하지 않음 expect(litMetadata?.links?.length || 0).toBe(0); // 영구 노트들이 문헌 노트를 참조하는지 추가 확인 const permanentNotesAgain = await executeTool( 'list_notes', { tags: ['permanent-note'] }, context ); expect(permanentNotesAgain._meta?.metadata as any).toHaveProperty('total', 2); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/inchan/memory-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server