UNPKG

meld

Version:

Meld: A template language for LLM prompts

676 lines (577 loc) 22.2 kB
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { StateVisualizationService } from './StateVisualizationService'; import { IStateHistoryService, StateOperation, StateTransformation } from '../StateHistoryService/IStateHistoryService'; import { IStateTrackingService, StateMetadata } from '../StateTrackingService/IStateTrackingService'; import { VisualizationConfig, VisualizationFormat } from './IStateVisualizationService'; describe('StateVisualizationService', () => { let mockHistoryService: IStateHistoryService & { [K in keyof IStateHistoryService]: Mock }; let mockTrackingService: IStateTrackingService & { [K in keyof IStateTrackingService]: Mock }; let visualizationService: StateVisualizationService; beforeEach(() => { mockHistoryService = { recordOperation: vi.fn(), getOperationHistory: vi.fn(), getTransformationChain: vi.fn(), queryHistory: vi.fn(), getRelatedOperations: vi.fn(), clearHistoryBefore: vi.fn(), }; mockTrackingService = { registerState: vi.fn(), addRelationship: vi.fn(), getStateLineage: vi.fn(), getStateDescendants: vi.fn(), }; visualizationService = new StateVisualizationService( mockHistoryService, mockTrackingService, ); }); describe('Hierarchy View Generation', () => { const mockMetadata: StateMetadata = { id: 'root', source: 'new', transformationEnabled: true, createdAt: Date.now(), }; beforeEach(() => { mockHistoryService.getOperationHistory.mockReturnValue([{ type: 'create', stateId: 'root', source: 'test', timestamp: Date.now(), metadata: mockMetadata, }]); }); it('generates mermaid format hierarchy', () => { const mockLineage = ['root', 'parent', 'child']; const mockDescendants = ['child1', 'child2']; mockTrackingService.getStateLineage.mockReturnValue(mockLineage); mockTrackingService.getStateDescendants.mockReturnValue(mockDescendants); const config: VisualizationConfig = { format: 'mermaid', includeMetadata: true, }; const result = visualizationService.generateHierarchyView('root', config); expect(result).toContain('graph TD;'); expect(result).toContain('root[new]'); // Check node format expect(result).toMatch(/style="box,#[0-9A-F]{6}"/); // Check styling expect(mockTrackingService.getStateLineage).toHaveBeenCalledWith('root'); expect(mockTrackingService.getStateDescendants).toHaveBeenCalledWith('root'); }); it('generates dot format hierarchy', () => { mockTrackingService.getStateLineage.mockReturnValue(['root']); mockTrackingService.getStateDescendants.mockReturnValue(['child']); const config: VisualizationConfig = { format: 'dot', includeMetadata: true, }; const result = visualizationService.generateHierarchyView('root', config); expect(result).toContain('digraph G {'); expect(result).toMatch(/"root" \[label="root\\nnew"/); // Check node format expect(result).toMatch(/shape="box"/); // Check styling }); it('generates json format with complete metadata', () => { mockTrackingService.getStateLineage.mockReturnValue(['root']); mockTrackingService.getStateDescendants.mockReturnValue([]); const config: VisualizationConfig = { format: 'json', includeMetadata: true, }; const result = visualizationService.generateHierarchyView('root', config); const parsed = JSON.parse(result); expect(parsed).toHaveProperty('nodes'); expect(parsed).toHaveProperty('edges'); expect(parsed.nodes[0]).toMatchObject({ id: 'root', source: 'new', }); }); it('handles empty state hierarchies gracefully', () => { mockTrackingService.getStateLineage.mockReturnValue([]); mockTrackingService.getStateDescendants.mockReturnValue([]); const config: VisualizationConfig = { format: 'mermaid', }; const result = visualizationService.generateHierarchyView('root', config); expect(result).toContain('graph TD;'); expect(result.split('\n')).toHaveLength(1); // Only contains header }); it('throws error for unsupported format', () => { const config = { format: 'invalid' as VisualizationFormat, }; expect(() => visualizationService.generateHierarchyView('root', config) ).toThrow('Unsupported format: invalid'); }); }); describe('Transition Diagram Generation', () => { it('visualizes state transformations', () => { const mockTransformations = [ { stateId: 'state1', timestamp: 1000, operation: 'update', source: 'test', before: { value: 1 }, after: { value: 2 }, }, ]; mockHistoryService.getTransformationChain.mockReturnValue(mockTransformations); const config: VisualizationConfig = { format: 'mermaid', includeTimestamps: true, }; const result = visualizationService.generateTransitionDiagram('state1', config); expect(mockHistoryService.getTransformationChain).toHaveBeenCalledWith('state1'); // TODO: Add more specific assertions once implementation is complete }); it('handles empty transformation chain', () => { mockHistoryService.getTransformationChain.mockReturnValue([]); const config: VisualizationConfig = { format: 'mermaid', }; const result = visualizationService.generateTransitionDiagram('state1', config); expect(result).toBe(''); // Or whatever empty state representation we decide }); }); describe('Timeline Generation', () => { const mockOperations: StateOperation[] = [ { type: 'create', stateId: 'state1', source: 'test', timestamp: 1000, }, { type: 'transform', stateId: 'state1', source: 'test', timestamp: 2000, }, ]; beforeEach(() => { mockHistoryService.getOperationHistory.mockReturnValue(mockOperations); }); it('generates timeline of operations', () => { const config: VisualizationConfig = { format: 'mermaid', includeTimestamps: true, }; const result = visualizationService.generateTimeline(['state1'], config); expect(mockHistoryService.getOperationHistory).toHaveBeenCalledWith('state1'); // TODO: Add more specific assertions once implementation is complete }); it('sorts operations by timestamp', () => { const config: VisualizationConfig = { format: 'mermaid', includeTimestamps: true, }; const result = visualizationService.generateTimeline(['state1'], config); // TODO: Verify sorting once implementation is complete }); }); describe('Metrics Calculation', () => { const mockOperations: StateOperation[] = [ { type: 'create', stateId: 'state1', source: 'test', timestamp: 1000, }, { type: 'transform', stateId: 'state1', source: 'test', timestamp: 2000, }, ]; beforeEach(() => { mockHistoryService.queryHistory.mockReturnValue(mockOperations); }); it('calculates system metrics within time range', () => { const timeRange = { start: 0, end: 3000, }; const metrics = visualizationService.getMetrics(timeRange); expect(mockHistoryService.queryHistory).toHaveBeenCalledWith({ timeRange }); expect(metrics).toHaveProperty('totalStates'); expect(metrics).toHaveProperty('statesByType'); expect(metrics).toHaveProperty('operationFrequency'); }); it('handles empty operation set', () => { mockHistoryService.queryHistory.mockReturnValue([]); const metrics = visualizationService.getMetrics(); expect(metrics.totalStates).toBe(0); expect(metrics.operationFrequency).toEqual({}); }); }); describe('Custom Styling', () => { const mockMetadata: StateMetadata = { id: 'root', source: 'new', transformationEnabled: true, createdAt: Date.now(), }; beforeEach(() => { mockHistoryService.getOperationHistory.mockReturnValue([{ type: 'create', stateId: 'root', source: 'test', timestamp: Date.now(), metadata: mockMetadata, }]); }); it('applies custom node styles', () => { mockTrackingService.getStateLineage.mockReturnValue(['root']); mockTrackingService.getStateDescendants.mockReturnValue([]); const config: VisualizationConfig = { format: 'dot', styleNodes: () => ({ shape: 'circle', color: '#FF0000', }), }; const result = visualizationService.generateHierarchyView('root', config); expect(result).toContain('circle'); expect(result).toContain('#FF0000'); }); it('applies custom edge styles', () => { mockTrackingService.getStateLineage.mockReturnValue(['root', 'child']); mockTrackingService.getStateDescendants.mockReturnValue([]); const config: VisualizationConfig = { format: 'dot', styleEdges: () => ({ style: 'dotted', color: '#00FF00', }), }; const result = visualizationService.generateHierarchyView('root', config); expect(result).toContain('dotted'); expect(result).toContain('#00FF00'); }); it('falls back to default styles when custom styling not provided', () => { mockTrackingService.getStateLineage.mockReturnValue(['root']); mockTrackingService.getStateDescendants.mockReturnValue([]); const config: VisualizationConfig = { format: 'dot', }; const result = visualizationService.generateHierarchyView('root', config); expect(result).toContain('box'); // Default shape expect(result).toMatch(/#[0-9A-F]{6}/); // Default color }); }); describe('Transformation Diagram Generation', () => { it('generates sequential transformation steps', () => { const transformations: StateTransformation[] = [ { stateId: 'state1', timestamp: 1000, operation: 'update', source: 'test', before: { value: 1 }, after: { value: 2 }, }, { stateId: 'state1', timestamp: 2000, operation: 'merge', source: 'test', before: { value: 2 }, after: { value: 3 }, }, ]; mockHistoryService.getTransformationChain.mockReturnValue(transformations); const result = visualizationService.generateTransitionDiagram('state1', { format: 'mermaid', includeTimestamps: true, }); // Verify transformation sequence expect(result).toContain('graph LR;'); // Left to right flow expect(result).toContain('value: 1'); expect(result).toContain('value: 2'); expect(result).toContain('value: 3'); expect(result).toContain('update'); expect(result).toContain('merge'); expect(result).toMatch(/1000.*update/); // Timestamp with operation expect(result).toMatch(/2000.*merge/); }); it('handles complex state values in transformations', () => { const transformations: StateTransformation[] = [ { stateId: 'state1', timestamp: 1000, operation: 'update', source: 'test', before: { nested: { value: 1, array: [1, 2] } }, after: { nested: { value: 2, array: [2, 3] } }, }, ]; mockHistoryService.getTransformationChain.mockReturnValue(transformations); const result = visualizationService.generateTransitionDiagram('state1', { format: 'mermaid', includeTimestamps: true, }); // Verify complex value handling expect(result).toContain('nested.value: 1'); expect(result).toContain('nested.value: 2'); expect(result).toContain('array: [1,2]'); expect(result).toContain('array: [2,3]'); }); }); describe('Timeline Generation', () => { it('handles overlapping operations from multiple states', () => { const state1Ops: StateOperation[] = [ { type: 'create', stateId: 'state1', source: 'test', timestamp: 1000 }, { type: 'transform', stateId: 'state1', source: 'test', timestamp: 3000 }, ]; const state2Ops: StateOperation[] = [ { type: 'create', stateId: 'state2', source: 'test', timestamp: 2000 }, { type: 'transform', stateId: 'state2', source: 'test', timestamp: 4000 }, ]; mockHistoryService.getOperationHistory .mockReturnValueOnce(state1Ops) .mockReturnValueOnce(state2Ops); const result = visualizationService.generateTimeline(['state1', 'state2'], { format: 'mermaid', includeTimestamps: true, }); // Verify timeline format expect(result).toContain('gantt'); expect(result).toMatch(/1000.*state1.*create/); expect(result).toMatch(/2000.*state2.*create/); expect(result).toMatch(/3000.*state1.*transform/); expect(result).toMatch(/4000.*state2.*transform/); }); it('groups operations by state in timeline', () => { const state1Ops: StateOperation[] = [ { type: 'create', stateId: 'state1', source: 'test', timestamp: 1000 }, { type: 'transform', stateId: 'state1', source: 'test', timestamp: 2000 }, ]; mockHistoryService.getOperationHistory.mockReturnValue(state1Ops); const result = visualizationService.generateTimeline(['state1'], { format: 'mermaid', includeTimestamps: true, }); // Verify state grouping expect(result).toContain('section state1'); expect(result).toMatch(/create.*1000/); expect(result).toMatch(/transform.*2000/); }); }); describe('Metrics Calculation', () => { it('calculates complex metrics correctly', () => { const operations: StateOperation[] = [ { type: 'create', stateId: 'state1', source: 'new', timestamp: 1000 }, { type: 'create', stateId: 'state2', source: 'clone', timestamp: 2000 }, { type: 'transform', stateId: 'state1', source: 'test', timestamp: 3000 }, { type: 'transform', stateId: 'state1', source: 'test', timestamp: 4000 }, { type: 'merge', stateId: 'state3', source: 'merge', timestamp: 5000 }, ]; mockHistoryService.queryHistory.mockReturnValue(operations); const metrics = visualizationService.getMetrics(); expect(metrics.totalStates).toBe(3); expect(metrics.statesByType).toEqual({ new: 1, clone: 1, merge: 1, }); expect(metrics.averageTransformationsPerState).toBe(2/3); // 2 transforms / 3 states expect(metrics.operationFrequency).toEqual({ create: 2, transform: 2, merge: 1, }); }); it('calculates tree depth metrics', () => { // Mock a tree structure: root -> child1 -> grandchild mockTrackingService.getStateLineage .mockReturnValueOnce(['root']) .mockReturnValueOnce(['root', 'child1']) .mockReturnValueOnce(['root', 'child1', 'grandchild']); const operations: StateOperation[] = [ { type: 'create', stateId: 'root', source: 'new', timestamp: 1000 }, { type: 'create', stateId: 'child1', source: 'new', timestamp: 2000 }, { type: 'create', stateId: 'grandchild', source: 'new', timestamp: 3000 }, ]; mockHistoryService.queryHistory.mockReturnValue(operations); const metrics = visualizationService.getMetrics(); expect(metrics.maxTreeDepth).toBe(3); // root -> child1 -> grandchild expect(metrics.averageChildrenPerState).toBe(1); // Each parent has 1 child }); }); describe('Relationship Graph Generation', () => { beforeEach(() => { // Mock state metadata mockHistoryService.getOperationHistory.mockImplementation((stateId) => { const operations: StateOperation[] = []; if (stateId === 'root') { operations.push({ type: 'create', stateId: 'root', source: 'new', timestamp: 1000, metadata: { id: 'root', source: 'new', transformationEnabled: true, createdAt: 1000, }, }); } else if (stateId === 'child1') { operations.push({ type: 'create', stateId: 'child1', source: 'clone', timestamp: 2000, metadata: { id: 'child1', source: 'clone', transformationEnabled: true, createdAt: 2000, }, }); } else if (stateId === 'merged') { operations.push({ type: 'merge', stateId: 'merged', source: 'merge', timestamp: 3000, parentId: 'root', metadata: { id: 'merged', source: 'merge', transformationEnabled: true, createdAt: 3000, }, }); } return operations; }); // Mock lineage and descendants mockTrackingService.getStateLineage.mockImplementation((stateId) => { switch (stateId) { case 'root': return ['root']; case 'child1': return ['root', 'child1']; case 'merged': return ['root', 'merged']; default: return []; } }); mockTrackingService.getStateDescendants.mockImplementation((stateId) => { switch (stateId) { case 'root': return ['child1', 'merged']; default: return []; } }); }); it('generates mermaid format relationship graph', () => { const result = visualizationService.generateRelationshipGraph(['root'], { format: 'mermaid', includeMetadata: true, }); // Check basic structure expect(result).toContain('graph TD;'); // Check nodes expect(result).toContain('root[new]'); expect(result).toContain('child1[clone]'); expect(result).toContain('merged[merge]'); // Check relationships expect(result).toMatch(/root.*-->.*child1/); expect(result).toMatch(/root.*-->.*merged/); // Check styling expect(result).toMatch(/style.*fill:#[0-9A-F]{6}/); expect(result).toMatch(/linkStyle.*stroke:/); }); it('generates dot format relationship graph', () => { const result = visualizationService.generateRelationshipGraph(['root'], { format: 'dot', includeMetadata: true, }); // Check basic structure expect(result).toContain('digraph G {'); expect(result).toContain('rankdir=TB;'); // Check nodes expect(result).toMatch(/"root".*label="root\\nnew"/); expect(result).toMatch(/"child1".*label="child1\\nclone"/); expect(result).toMatch(/"merged".*label="merged\\nmerge"/); // Check relationships expect(result).toMatch(/"root".*->.*"child1"/); expect(result).toMatch(/"root".*->.*"merged"/); // Check styling expect(result).toMatch(/shape="[^"]+"/); expect(result).toMatch(/color="#[0-9A-F]{6}"/); expect(result).toMatch(/style="[^"]+"/); }); it('generates json format relationship graph', () => { const result = visualizationService.generateRelationshipGraph(['root'], { format: 'json', includeMetadata: true, }); const parsed = JSON.parse(result); // Check structure expect(parsed).toHaveProperty('nodes'); expect(parsed).toHaveProperty('edges'); // Check nodes expect(parsed.nodes).toHaveLength(3); // root, child1, merged expect(parsed.nodes.find((n: any) => n.id === 'root')).toBeTruthy(); expect(parsed.nodes.find((n: any) => n.id === 'child1')).toBeTruthy(); expect(parsed.nodes.find((n: any) => n.id === 'merged')).toBeTruthy(); // Check edges expect(parsed.edges).toContainEqual(expect.objectContaining({ sourceId: 'root', targetId: 'child1', type: 'parent-child', })); expect(parsed.edges).toContainEqual(expect.objectContaining({ sourceId: 'root', targetId: 'merged', type: 'merge-source', })); }); it('handles cycles in state relationships', () => { // Mock a cyclic relationship mockTrackingService.getStateLineage.mockImplementation((stateId) => { switch (stateId) { case 'state1': return ['state1', 'state2']; case 'state2': return ['state2', 'state1']; default: return []; } }); const result = visualizationService.generateRelationshipGraph(['state1', 'state2'], { format: 'json', includeMetadata: true, }); const parsed = JSON.parse(result); expect(parsed.nodes).toBeDefined(); expect(parsed.edges).toBeDefined(); // Should not enter infinite recursion }); it('handles empty state set', () => { const result = visualizationService.generateRelationshipGraph([], { format: 'mermaid', includeMetadata: true, }); expect(result).toContain('graph TD;'); expect(result.split('\n')).toHaveLength(1); // Only contains header }); it('throws error for unsupported format', () => { expect(() => visualizationService.generateRelationshipGraph(['root'], { format: 'invalid' as any, includeMetadata: true, }) ).toThrow('Unsupported format: invalid'); }); }); });