Files

382 lines
12 KiB
TypeScript

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<string, string[]> = {
'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<string, string[]> = {
'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<string, string[]> = {
'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 {');
});
});
});