import { extractConcepts, findRelationships, buildDependencyGraph, toDotFormat, toJsonFormat, toCytoscapeFormat, generateGraphVisualization, } from '../src/graph-view'; import type { VaultIndexEntry } from '../src/types'; import type { DependencyGraph } from '../src/graph-view'; describe('Graph Utilities', () => { const mockFiles: VaultIndexEntry[] = [ { path: 'algorithms.md', title: 'Algorithms', content: '# Sorting Algorithms\n\n## Bubble Sort\nThe **bubble sort** is a simple sorting algorithm.\n\n## Quick Sort\n*Quick sort* is more efficient.', score: 1, }, { path: 'data-structures.md', title: 'Data Structures', content: '# Data Structures\n\n## Trees\nBinary trees are fundamental.\n\n## Graphs\nGraph algorithms build on *Sorting Algorithms*.', score: 2, }, { path: 'patterns.md', title: 'Design Patterns', content: '# Design Patterns\n\n## Factory Pattern\nCreates objects.\n\n## Observer Pattern\n**bubble sort** mentions the Factory Pattern.', score: 3, }, ]; describe('extractConcepts', () => { it('should extract headings as concepts', () => { const content = '# Main Heading\n## Sub Heading\n### Deep Heading'; const concepts = extractConcepts(content, 'test.md'); expect(concepts).toContain('Main Heading'); expect(concepts).toContain('Sub Heading'); expect(concepts).toContain('Deep Heading'); }); it('should extract bold text as concepts', () => { const content = 'This is **important concept** and **another concept**.'; const concepts = extractConcepts(content, 'test.md'); expect(concepts).toContain('important concept'); expect(concepts).toContain('another concept'); }); it('should not duplicate concepts', () => { const content = '# Heading\n\n# Heading\n\n**Heading**'; const concepts = extractConcepts(content, 'test.md'); expect(concepts).toEqual(['Heading']); }); it('should return empty array for content without concepts', () => { const content = 'Plain text without any special formatting.'; const concepts = extractConcepts(content, 'test.md'); expect(concepts).toEqual([]); }); it('should handle empty content', () => { const concepts = extractConcepts('', 'test.md'); expect(concepts).toEqual([]); }); it('should extract all heading levels', () => { const content = '# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6'; const concepts = extractConcepts(content, 'test.md'); expect(concepts).toEqual(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']); }); }); describe('findRelationships', () => { it('should find relationships between files sharing concepts', () => { const conceptIndex: Record = { 'Sorting Algorithms': ['algorithms.md'], 'bubble sort': ['algorithms.md', 'patterns.md'], }; const relationships = findRelationships(mockFiles, conceptIndex); // Algorithms.md references patterns.md (via "bubble sort") // Data-structures.md references algorithms.md (via "Sorting Algorithms") // Patterns.md references algorithms.md (via "bubble sort") expect(relationships).toHaveLength(3); expect( relationships.some((r) => r.source === 'patterns.md' && r.target === 'algorithms.md') ).toBe(true); expect( relationships.some((r) => r.source === 'data-structures.md' && r.target === 'algorithms.md') ).toBe(true); expect( relationships.some((r) => r.source === 'algorithms.md' && r.target === 'patterns.md') ).toBe(true); }); it('should exclude self-references', () => { const conceptIndex: Record = { 'bubble sort': ['algorithms.md'], }; const relationships = findRelationships(mockFiles, conceptIndex); const selfRefs = relationships.filter((r) => r.source === r.target); expect(selfRefs).toHaveLength(0); }); it('should return empty array for empty files', () => { const relationships = findRelationships([], {}); expect(relationships).toEqual([]); }); it('should return empty array when no concepts overlap', () => { const conceptIndex: Record = { 'Unique Concept A': ['file1.md'], 'Unique Concept B': ['file2.md'], }; const isolatedFiles: VaultIndexEntry[] = [ { path: 'file1.md', title: 'File 1', content: '# Unique Concept A', score: 1 }, { path: 'file2.md', title: 'File 2', content: '# Unique Concept B', score: 2 }, ]; const relationships = findRelationships(isolatedFiles, conceptIndex); expect(relationships).toEqual([]); }); }); describe('buildDependencyGraph', () => { it('should create nodes for each file', () => { const graph = buildDependencyGraph(mockFiles); expect(graph.nodes).toHaveLength(3); expect(graph.nodes.map((n) => n.id)).toEqual([ 'algorithms.md', 'data-structures.md', 'patterns.md', ]); }); it('should set node type to file', () => { const graph = buildDependencyGraph(mockFiles); graph.nodes.forEach((node) => { expect(node.type).toBe('file'); }); }); it('should include file properties', () => { const graph = buildDependencyGraph(mockFiles); const node = graph.nodes[0]; expect(node.properties).toEqual({ path: 'algorithms.md', title: 'Algorithms', contentPreview: expect.stringContaining('Sorting Algorithms'), }); }); it('should create edges for concept relationships', () => { const filesWithRelationship: VaultIndexEntry[] = [ { path: 'file1.md', title: 'File 1', content: '# Shared Concept\n\nSome content about **Shared Concept**.', score: 1, }, { path: 'file2.md', title: 'File 2', content: '# Shared Concept\n\nMore content.', score: 2, }, ]; const graph = buildDependencyGraph(filesWithRelationship); expect(graph.edges).toHaveLength(2); // Each mentions the other's concept }); it('should handle empty file list', () => { const graph = buildDependencyGraph([]); expect(graph.nodes).toEqual([]); expect(graph.edges).toEqual([]); }); it('should truncate content preview to 100 chars', () => { const longFile: VaultIndexEntry = { path: 'long.md', title: 'Long', content: 'A'.repeat(200), score: 1, }; const graph = buildDependencyGraph([longFile]); expect(graph.nodes[0].properties.contentPreview).toHaveLength(101); // 100 + '.' }); }); describe('toDotFormat', () => { it('should generate valid DOT syntax', () => { const graph: DependencyGraph = { nodes: [ { id: 'node1', label: 'Node 1', file: { path: 'file1.md', title: 'File 1', content: 'Content', score: 1 }, type: 'file', properties: {}, }, { id: 'node2', label: 'Node 2', file: { path: 'file2.md', title: 'File 2', content: 'Content', score: 2 }, type: 'file', properties: {}, }, ], edges: [ { id: 'edge1', source: 'node1', target: 'node2', label: 'references', relationship: 'references', properties: {}, }, ], }; const dot = toDotFormat(graph); expect(dot).toContain('digraph G {'); expect(dot).toContain('"node1" [label="Node 1"]'); expect(dot).toContain('"node2" [label="Node 2"]'); expect(dot).toContain('"node1" -> "node2" [label="references"]'); expect(dot).toContain('}'); }); it('should handle empty graph', () => { const dot = toDotFormat({ nodes: [], edges: [] }); expect(dot).toContain('digraph G {'); expect(dot).toContain('}'); }); it('should escape quotes in labels', () => { const graph: DependencyGraph = { nodes: [ { id: 'node1', label: 'Node with "quotes"', file: { path: 'file1.md', title: 'File 1', content: 'C', score: 1 }, type: 'file', properties: {}, }, ], edges: [], }; const dot = toDotFormat(graph); expect(dot).toContain('label="Node with \\"quotes\\""'); }); }); describe('toJsonFormat', () => { it('should produce valid JSON', () => { const graph: DependencyGraph = { nodes: [ { id: 'n1', label: 'Node', file: { path: 'f.md', title: 'T', content: 'C', score: 1 }, type: 'file', properties: {}, }, ], edges: [], }; const json = toJsonFormat(graph); const parsed = JSON.parse(json); expect(parsed.nodes).toHaveLength(1); expect(parsed.edges).toEqual([]); }); it('should include all node and edge data', () => { const graph: DependencyGraph = { nodes: [], edges: [ { id: 'e1', source: 's', target: 't', label: 'rel', relationship: 'rel', properties: {}, }, ], }; const json = toJsonFormat(graph); const parsed = JSON.parse(json); expect(parsed.edges[0].source).toBe('s'); expect(parsed.edges[0].target).toBe('t'); }); }); describe('toCytoscapeFormat', () => { it('should produce valid Cytoscape JSON structure', () => { const graph: DependencyGraph = { nodes: [ { id: 'n1', label: 'Node', file: { path: 'f.md', title: 'T', content: 'C', score: 1 }, type: 'file', properties: { extra: true }, }, ], edges: [], }; const json = toCytoscapeFormat(graph); const parsed = JSON.parse(json); expect(parsed.elements.nodes).toHaveLength(1); expect(parsed.elements.nodes[0].data.id).toBe('n1'); expect(parsed.elements.nodes[0].data.type).toBe('file'); expect(parsed.elements.nodes[0].data.extra).toBe(true); }); it('should include edge data in Cytoscape format', () => { const graph: DependencyGraph = { nodes: [], edges: [ { id: 'e1', source: 's', target: 't', label: 'rel', relationship: 'rel', properties: {}, }, ], }; const json = toCytoscapeFormat(graph); const parsed = JSON.parse(json); expect(parsed.elements.edges[0].data.source).toBe('s'); expect(parsed.elements.edges[0].data.target).toBe('t'); expect(parsed.elements.edges[0].data.relationship).toBe('rel'); }); }); describe('generateGraphVisualization', () => { it('should generate DOT format by default', () => { const result = generateGraphVisualization(mockFiles); expect(result).toContain('digraph G {'); }); it('should generate DOT format when specified', () => { const result = generateGraphVisualization(mockFiles, 'dot'); expect(result).toContain('digraph G {'); }); it('should generate JSON format when specified', () => { const result = generateGraphVisualization(mockFiles, 'json'); const parsed = JSON.parse(result); expect(parsed.nodes).toBeDefined(); expect(parsed.edges).toBeDefined(); }); it('should generate Cytoscape format when specified', () => { const result = generateGraphVisualization(mockFiles, 'cytoscape'); const parsed = JSON.parse(result); expect(parsed.elements).toBeDefined(); expect(parsed.elements.nodes).toBeDefined(); }); it('should fall back to DOT for unknown formats', () => { const result = generateGraphVisualization(mockFiles, 'dot' as any); expect(result).toContain('digraph G {'); }); }); });