meld
Version:
Meld: A template language for LLM prompts
477 lines (406 loc) • 16.2 kB
text/typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { StateTrackingService } from './StateTrackingService.js';
import type { StateMetadata } from './IStateTrackingService.js';
import { StateVisualizationService } from '../StateVisualizationService/StateVisualizationService.js';
import { StateDebuggerService } from '../StateDebuggerService/StateDebuggerService.js';
import { StateHistoryService } from '../StateHistoryService/StateHistoryService.js';
import type { IStateHistoryService } from '../StateHistoryService/IStateHistoryService.js';
import type { IStateEventService } from '@services/state/StateEventService/IStateEventService.js';
class MockStateEventService implements IStateEventService {
private handlers = new Map<string, Array<{
handler: (event: any) => void | Promise<void>;
options?: { filter?: (event: any) => boolean };
}>>();
constructor() {
['create', 'clone', 'transform', 'merge', 'error'].forEach(type => {
this.handlers.set(type, []);
});
}
on(type: string, handler: (event: any) => void | Promise<void>, options?: { filter?: (event: any) => boolean }): void {
const handlers = this.handlers.get(type);
if (handlers) {
handlers.push({ handler, options });
}
}
off(type: string, handler: (event: any) => void | Promise<void>): void {
const handlers = this.handlers.get(type);
if (handlers) {
const index = handlers.findIndex(h => h.handler === handler);
if (index !== -1) {
handlers.splice(index, 1);
}
}
}
async emit(event: any): Promise<void> {
const handlers = this.handlers.get(event.type) || [];
for (const { handler, options } of handlers) {
if (!options?.filter || options.filter(event)) {
await Promise.resolve(handler(event));
}
}
}
}
describe('StateTrackingService', () => {
let service: StateTrackingService;
beforeEach(() => {
service = new StateTrackingService();
});
describe('State Registration', () => {
it('should register a new state with generated ID', () => {
const metadata: Partial<StateMetadata> = {
source: 'new',
transformationEnabled: true
};
const stateId = service.registerState(metadata);
expect(stateId).toBeDefined();
expect(typeof stateId).toBe('string');
expect(stateId.length).toBeGreaterThan(0);
});
it('should store complete metadata', () => {
const metadata: Partial<StateMetadata> = {
source: 'clone',
parentId: 'parent-123',
filePath: 'test.meld',
transformationEnabled: true
};
const stateId = service.registerState(metadata);
const stored = service.getStateMetadata(stateId);
expect(stored).toBeDefined();
expect(stored?.source).toBe('clone');
expect(stored?.parentId).toBe('parent-123');
expect(stored?.filePath).toBe('test.meld');
expect(stored?.transformationEnabled).toBe(true);
});
});
describe('State Lineage', () => {
it('should return empty array for non-existent state', () => {
const lineage = service.getStateLineage('non-existent');
expect(lineage).toEqual([]);
});
it('should return single state for root state', () => {
const rootId = service.registerState({
source: 'new',
transformationEnabled: true
});
const lineage = service.getStateLineage(rootId);
expect(lineage).toEqual([rootId]);
});
it('should return correct lineage for simple parent-child relationship', () => {
const rootId = service.registerState({
source: 'new',
transformationEnabled: true
});
const childId = service.registerState({
source: 'child',
parentId: rootId,
transformationEnabled: true
});
service.addRelationship(rootId, childId, 'parent-child');
const lineage = service.getStateLineage(childId);
expect(lineage).toEqual([rootId, childId]);
});
it('should handle multi-level lineage', () => {
const rootId = service.registerState({
source: 'new',
transformationEnabled: true
});
const child1Id = service.registerState({
source: 'child',
parentId: rootId,
transformationEnabled: true
});
const child2Id = service.registerState({
source: 'child',
parentId: child1Id,
transformationEnabled: true
});
service.addRelationship(rootId, child1Id, 'parent-child');
service.addRelationship(child1Id, child2Id, 'parent-child');
const lineage = service.getStateLineage(child2Id);
expect(lineage).toEqual([rootId, child1Id, child2Id]);
});
it('should handle circular relationships', () => {
const state1Id = service.registerState({
source: 'new',
transformationEnabled: true
});
const state2Id = service.registerState({
source: 'child',
parentId: state1Id,
transformationEnabled: true
});
const state3Id = service.registerState({
source: 'child',
parentId: state2Id,
transformationEnabled: true
});
service.addRelationship(state1Id, state2Id, 'parent-child');
service.addRelationship(state2Id, state3Id, 'parent-child');
service.addRelationship(state3Id, state1Id, 'parent-child'); // Creates a cycle
const lineage = service.getStateLineage(state3Id);
expect(lineage).toEqual([state1Id, state2Id, state3Id]);
});
});
describe('State Descendants', () => {
it('should return empty array for non-existent state', () => {
const descendants = service.getStateDescendants('non-existent');
expect(descendants).toEqual([]);
});
it('should return empty array for leaf state', () => {
const leafId = service.registerState({
source: 'new',
transformationEnabled: true
});
const descendants = service.getStateDescendants(leafId);
expect(descendants).toEqual([]);
});
it('should return immediate children', () => {
const rootId = service.registerState({
source: 'new',
transformationEnabled: true
});
const child1Id = service.registerState({
source: 'child',
parentId: rootId,
transformationEnabled: true
});
const child2Id = service.registerState({
source: 'child',
parentId: rootId,
transformationEnabled: true
});
service.addRelationship(rootId, child1Id, 'parent-child');
service.addRelationship(rootId, child2Id, 'parent-child');
const descendants = service.getStateDescendants(rootId);
expect(descendants).toContain(child1Id);
expect(descendants).toContain(child2Id);
expect(descendants.length).toBe(2);
});
it('should return all descendants in complex hierarchy', () => {
const rootId = service.registerState({
source: 'new',
transformationEnabled: true
});
const child1Id = service.registerState({
source: 'child',
parentId: rootId,
transformationEnabled: true
});
const child2Id = service.registerState({
source: 'child',
parentId: rootId,
transformationEnabled: true
});
const grandchild1Id = service.registerState({
source: 'child',
parentId: child1Id,
transformationEnabled: true
});
const grandchild2Id = service.registerState({
source: 'child',
parentId: child2Id,
transformationEnabled: true
});
service.addRelationship(rootId, child1Id, 'parent-child');
service.addRelationship(rootId, child2Id, 'parent-child');
service.addRelationship(child1Id, grandchild1Id, 'parent-child');
service.addRelationship(child2Id, grandchild2Id, 'parent-child');
const descendants = service.getStateDescendants(rootId);
expect(descendants).toContain(child1Id);
expect(descendants).toContain(child2Id);
expect(descendants).toContain(grandchild1Id);
expect(descendants).toContain(grandchild2Id);
expect(descendants.length).toBe(4);
});
});
describe('Merge Operations', () => {
let service: StateTrackingService;
let trackingService: IStateTrackingService;
let eventService: MockStateEventService;
let visualizationService: StateVisualizationService;
let debuggerService: StateDebuggerService;
let historyService: StateHistoryService;
beforeEach(() => {
service = new StateTrackingService();
eventService = new MockStateEventService();
trackingService = service; // StateTrackingService is itself the tracking service
historyService = new StateHistoryService(eventService);
visualizationService = new StateVisualizationService(historyService, trackingService);
debuggerService = new StateDebuggerService(visualizationService, historyService, trackingService);
// Set up bidirectional service connections
(service as any).eventService = eventService;
(service as any).services = {
visualization: visualizationService,
debugger: debuggerService,
history: historyService,
events: eventService
};
});
it('should handle merge source relationships', () => {
const sourceId = service.registerState({
source: 'new',
transformationEnabled: true
});
const targetId = service.registerState({
source: 'new',
transformationEnabled: true
});
service.addRelationship(sourceId, targetId, 'merge-source');
const descendants = service.getStateDescendants(sourceId);
expect(descendants).toContain(targetId);
});
it('should handle merge target relationships', async () => {
// Start debug session with enhanced configuration
const debugSessionId = await debuggerService.startSession({
captureConfig: {
capturePoints: ['pre-transform', 'post-transform', 'error'],
includeFields: ['nodes', 'transformedNodes', 'variables', 'metadata', 'relationships'],
format: 'full'
},
visualization: {
format: 'mermaid',
includeMetadata: true,
includeTimestamps: true
}
});
try {
// Create initial states with event emission
const sourceId = service.registerState({
source: 'new',
transformationEnabled: true
});
console.log('Created source state:', sourceId);
eventService.emit('create', { type: 'create', stateId: sourceId, source: 'registerState' });
const targetId = service.registerState({
source: 'new',
transformationEnabled: true
});
console.log('Created target state:', targetId);
eventService.emit('create', { type: 'create', stateId: targetId, source: 'registerState' });
const parentId = service.registerState({
source: 'new',
transformationEnabled: true
});
console.log('Created parent state:', parentId);
eventService.emit('create', { type: 'create', stateId: parentId, source: 'registerState' });
// Visualize initial states
console.log('\nInitial States:');
console.log(await visualizationService.generateHierarchyView(sourceId, {
format: 'mermaid',
includeMetadata: true
}));
// Add parent-child relationship
service.addRelationship(parentId, targetId, 'parent-child');
console.log('\nAdded parent-child relationship:', {
parent: parentId,
child: targetId,
parentMetadata: service.getStateMetadata(parentId),
childMetadata: service.getStateMetadata(targetId),
parentRelationships: service.getRelationships(parentId),
childRelationships: service.getRelationships(targetId)
});
eventService.emit('transform', {
type: 'transform',
source: 'addRelationship:parent-child',
stateId: parentId,
targetId: targetId
});
// Visualize after parent-child relationship
console.log('\nAfter Parent-Child Relationship:');
console.log(await visualizationService.generateHierarchyView(parentId, {
format: 'mermaid',
includeMetadata: true
}));
// Add merge-target relationship
service.addRelationship(sourceId, targetId, 'merge-target');
console.log('\nAdded merge-target relationship:', {
source: sourceId,
target: targetId,
sourceMetadata: service.getStateMetadata(sourceId),
targetMetadata: service.getStateMetadata(targetId),
sourceRelationships: service.getRelationships(sourceId),
targetRelationships: service.getRelationships(targetId)
});
eventService.emit('transform', {
type: 'transform',
source: 'addRelationship:merge-target',
stateId: sourceId,
targetId: targetId
});
// Visualize after merge-target relationship
console.log('\nAfter Merge-Target Relationship:');
console.log(await visualizationService.generateHierarchyView(sourceId, {
format: 'mermaid',
includeMetadata: true
}));
// Generate transition diagram
console.log('\nState Transitions:');
console.log(await visualizationService.generateTransitionDiagram(sourceId, {
format: 'mermaid',
includeTimestamps: true
}));
// Get and verify lineage
const lineage = service.getStateLineage(sourceId);
console.log('\nState Lineage:', {
sourceId,
targetId,
parentId,
lineage,
sourceMetadata: service.getStateMetadata(sourceId),
targetMetadata: service.getStateMetadata(targetId),
parentMetadata: service.getStateMetadata(parentId),
sourceRelationships: service.getRelationships(sourceId),
targetRelationships: service.getRelationships(targetId),
parentRelationships: service.getRelationships(parentId)
});
// Verify lineage includes parent
expect(lineage).toContain(parentId);
// Get and log complete debug report
const report = await debuggerService.generateDebugReport(debugSessionId);
console.log('\nComplete Debug Report:', report);
} catch (error) {
// Log error diagnostics
const errorReport = await debuggerService.generateDebugReport(debugSessionId);
console.error('Error Debug Report:', errorReport);
throw error;
} finally {
await debuggerService.endSession(debugSessionId);
}
});
it('should handle complex merge scenarios', () => {
// Create a complex hierarchy with merges
const rootId = service.registerState({
source: 'new',
transformationEnabled: true
});
const branch1Id = service.registerState({
source: 'child',
parentId: rootId,
transformationEnabled: true
});
const branch2Id = service.registerState({
source: 'child',
parentId: rootId,
transformationEnabled: true
});
const mergeTargetId = service.registerState({
source: 'merge',
transformationEnabled: true
});
// Set up relationships
service.addRelationship(rootId, branch1Id, 'parent-child');
service.addRelationship(rootId, branch2Id, 'parent-child');
service.addRelationship(branch1Id, mergeTargetId, 'merge-source');
service.addRelationship(branch2Id, mergeTargetId, 'merge-target');
// Verify lineage
const lineage = service.getStateLineage(mergeTargetId);
expect(lineage).toContain(rootId);
expect(lineage).toContain(branch1Id);
// Verify descendants
const descendants = service.getStateDescendants(rootId);
expect(descendants).toContain(branch1Id);
expect(descendants).toContain(branch2Id);
expect(descendants).toContain(mergeTargetId);
});
});
});