UNPKG

meld

Version:

Meld: A template language for LLM prompts

511 lines (434 loc) 21.7 kB
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { TestContext } from '@tests/utils/index.js'; import { MeldInterpreterError } from '@core/errors/MeldInterpreterError.js'; import { DirectiveError } from '@services/pipeline/DirectiveService/errors/DirectiveError.js'; import { MeldImportError } from '@core/errors/MeldImportError.js'; import type { TextNode, MeldNode, DirectiveNode } from 'meld-spec'; // Import centralized syntax helpers import { createNodeFromExample } from '@core/syntax/helpers'; // Import relevant examples import { textDirectiveExamples, dataDirectiveExamples, pathDirectiveExamples, importDirectiveExamples, defineDirectiveExamples, integrationExamples } from '@core/syntax'; import { IInterpreterService } from '@services/pipeline/InterpreterService/IInterpreterService.js'; import { InterpreterService } from '@services/pipeline/InterpreterService/InterpreterService.js'; describe('InterpreterService Integration', () => { let context: TestContext; beforeEach(async () => { context = new TestContext(); await context.initialize(); await context.fixtures.load('interpreterTestProject'); }); afterEach(async () => { await context.cleanup(); }); describe('Basic interpretation', () => { it('interprets text nodes', async () => { const content = 'Hello world'; const nodes = await context.services.parser.parse(content); const result = await context.services.interpreter.interpret(nodes); const resultNodes = result.getNodes(); expect(resultNodes).toHaveLength(1); expect(resultNodes[0].type).toBe('Text'); expect((resultNodes[0] as TextNode).content).toBe('Hello world'); }); it('interprets directive nodes', async () => { // MIGRATION: Using centralized syntax example instead of hardcoded directive const example = textDirectiveExamples.atomic.simpleString; const node = await createNodeFromExample(example.code); const result = await context.services.interpreter.interpret([node]); // Extract the expected variable name from the example (should be 'test' in this example) const varName = node.directive.identifier; const value = result.getTextVar(varName); // Check if the value is set correctly // For text directives, the value should be a string expect(typeof value).toBe('string'); expect(value).toBeTruthy(); }); it('interprets data directives', async () => { // MIGRATION: Using centralized syntax example instead of hardcoded directive const example = dataDirectiveExamples.atomic.simpleObject; const node = await createNodeFromExample(example.code); const result = await context.services.interpreter.interpret([node]); // Extract the variable name from the example const varName = node.directive.identifier; const value = result.getDataVar(varName); // Verify the data is an object expect(value).toBeDefined(); expect(typeof value).toBe('object'); // Data should not be null expect(value).not.toBeNull(); }); it('interprets path directives', async () => { // Create a path directive with a valid path that follows the rules // Simple paths (no slashes) are valid, or use a path variable for paths with slashes const node = context.factory.createPathDirective('testPath', 'docs'); const result = await context.services.interpreter.interpret([node]); // Extract the variable name from the node const varName = node.directive.identifier; const value = result.getPathVar(varName); // Verify path value exists expect(value).toBeDefined(); expect(typeof value === 'string' || (typeof value === 'object' && value !== null)).toBe(true); }); it('maintains node order in state', async () => { const nodes = [ context.factory.createTextDirective('first', 'one', context.factory.createLocation(1, 1)), context.factory.createTextDirective('second', 'two', context.factory.createLocation(2, 1)), context.factory.createTextDirective('third', 'three', context.factory.createLocation(3, 1)) ]; // Create a parent state to track nodes const parentState = context.services.state.createChildState(); const result = await context.services.interpreter.interpret(nodes, { initialState: parentState, filePath: 'test.meld', mergeState: true }); const stateNodes = result.getNodes(); expect(stateNodes).toHaveLength(3); expect(stateNodes[0].type).toBe('Directive'); expect((stateNodes[0] as any).directive.identifier).toBe('first'); expect(stateNodes[1].type).toBe('Directive'); expect((stateNodes[1] as any).directive.identifier).toBe('second'); expect(stateNodes[2].type).toBe('Directive'); expect((stateNodes[2] as any).directive.identifier).toBe('third'); }); }); describe('State management', () => { it('creates isolated states for different interpretations', async () => { const node = context.factory.createTextDirective('test', 'value'); const result1 = await context.services.interpreter.interpret([node]); const result2 = await context.services.interpreter.interpret([node]); expect(result1).not.toBe(result2); expect(result1.getTextVar('test')).toBe('value'); expect(result2.getTextVar('test')).toBe('value'); }); it('merges child state back to parent', async () => { const node = context.factory.createTextDirective('child', 'value'); const parentState = context.services.state.createChildState(); await context.services.interpreter.interpret([node], { initialState: parentState, mergeState: true }); expect(parentState.getTextVar('child')).toBe('value'); }); it('maintains isolation with mergeState: false', async () => { const node = context.factory.createTextDirective('isolated', 'value'); const parentState = context.services.state.createChildState(); await context.services.interpreter.interpret([node], { initialState: parentState, mergeState: false }); expect(parentState.getTextVar('isolated')).toBeUndefined(); }); it('handles state rollback on merge errors', async () => { // Create a directive that will cause a resolution error const node = context.factory.createTextDirective('error', '{{nonexistent}}', context.factory.createLocation(1, 1)); // Create parent state with initial value const parentState = context.services.state.createChildState(); parentState.setTextVar('original', 'value'); try { await context.services.interpreter.interpret([node], { initialState: parentState, filePath: 'test.meld', mergeState: true }); throw new Error('Should have thrown error'); } catch (error) { if (error instanceof MeldInterpreterError) { // Verify error details expect(error.nodeType).toBe('Directive'); expect(error.message).toMatch(/Directive error \(text\)/i); if (error.cause?.message) { expect(error.cause.message).toMatch(/Failed to resolve string concatenation/i); } // Verify state was rolled back expect(parentState.getTextVar('original')).toBe('value'); expect(parentState.getTextVar('error')).toBeUndefined(); } else { throw error; } } }); }); describe('Error handling', () => { it('handles circular imports', async () => { // Create a mock circular import setup await context.writeFile('project/src/circular1.meld', '@import [$./circular2.meld]'); await context.writeFile('project/src/circular2.meld', '@import [$./circular1.meld]'); // Create an import directive node for the interpreter // MIGRATION NOTE: Using factory method directly due to issues with examples for circular imports const node = context.factory.createImportDirective( '$./project/src/circular1.meld', context.factory.createLocation(1, 1) ); // Mock the CircularityService to throw a circular import error const originalBeginImport = context.services.circularity.beginImport; context.services.circularity.beginImport = (filePath: string) => { throw new MeldImportError('Circular import detected', { code: 'CIRCULAR_IMPORT', details: { importChain: ['a.meld', 'b.meld', 'a.meld'] } }); }; try { // This should throw an error due to the circular import await context.services.interpreter.interpret([node], { filePath: 'test.meld' }); throw new Error('Should have thrown error'); } catch (error: unknown) { if (error instanceof MeldInterpreterError) { expect(error.message).toContain('Circular import'); } else { throw error; } } finally { // Restore original functionality context.services.circularity.beginImport = originalBeginImport; } }); it('provides location information in errors', async () => { // MIGRATION: Using centralized invalid example for undefined variable const example = textDirectiveExamples.invalid.undefinedVariable; const node = await createNodeFromExample(example.code); try { await context.services.interpreter.interpret([node], { filePath: 'test.meld' }); throw new Error('Should have thrown error'); } catch (error: unknown) { if (error instanceof MeldInterpreterError) { expect(error).toBeInstanceOf(MeldInterpreterError); expect(error.location).toBeDefined(); expect(error.location?.line).toBe(1); expect(error.location?.column).toBe(2); } else { throw error; } } }); it('maintains state consistency after errors', async () => { // MIGRATION: Using centralized valid and invalid examples const validExample = textDirectiveExamples.atomic.simpleString; const validNode = await createNodeFromExample(validExample.code); const invalidExample = textDirectiveExamples.invalid.undefinedVariable; const invalidNode = await createNodeFromExample(invalidExample.code); const nodes = [validNode, invalidNode]; try { await context.services.interpreter.interpret(nodes, { initialState: context.services.state.createChildState(), filePath: 'test.meld' }); throw new Error('Should have thrown error'); } catch (error: unknown) { if (error instanceof MeldInterpreterError) { // Verify state was rolled back expect(context.services.state.getTextVar(validNode.directive.identifier)).toBeUndefined(); expect(context.services.state.getTextVar('error')).toBeUndefined(); } else { throw error; } } }); it('includes state context in interpreter errors', async () => { // MIGRATION: Using centralized invalid example for undefined variable const example = textDirectiveExamples.invalid.undefinedVariable; const node = await createNodeFromExample(example.code); try { await context.services.interpreter.interpret([node], { filePath: 'test.meld' }); throw new Error('Should have thrown error'); } catch (error: unknown) { if (error instanceof MeldInterpreterError) { expect(error).toBeInstanceOf(MeldInterpreterError); expect(error.context).toBeDefined(); if (error.context) { expect(error.context.nodeType).toBe('Directive'); expect(error.context.state?.filePath).toBe('test.meld'); } } else { throw error; } } }); it('rolls back state on directive errors', async () => { // MIGRATION: Create nodes using centralized examples const beforeExample = textDirectiveExamples.atomic.simpleString; const beforeNode = await createNodeFromExample(beforeExample.code); const errorExample = textDirectiveExamples.invalid.undefinedVariable; const errorNode = await createNodeFromExample(errorExample.code); const afterExample = textDirectiveExamples.atomic.user; const afterNode = await createNodeFromExample(afterExample.code); const nodes = [beforeNode, errorNode, afterNode]; try { await context.services.interpreter.interpret(nodes, { initialState: context.services.state.createChildState(), filePath: 'test.meld' }); throw new Error('Should have thrown error'); } catch (error: unknown) { if (error instanceof MeldInterpreterError) { // Verify state was rolled back expect(context.services.state.getTextVar(beforeNode.directive.identifier)).toBeUndefined(); expect(context.services.state.getTextVar('error')).toBeUndefined(); expect(context.services.state.getTextVar(afterNode.directive.identifier)).toBeUndefined(); } else { throw error; } } }); it('handles cleanup on circular imports', async () => { // Create a mock circular import setup await context.writeFile('project/src/circular1.meld', '@import [$./circular2.meld]'); await context.writeFile('project/src/circular2.meld', '@import [$./circular1.meld]'); // Create an import directive node for the interpreter // MIGRATION NOTE: Using factory method directly due to issues with examples for circular imports const node = context.factory.createImportDirective( '$./project/src/circular1.meld', context.factory.createLocation(1, 1) ); // Mock the CircularityService to throw a circular import error const originalBeginImport = context.services.circularity.beginImport; context.services.circularity.beginImport = (filePath: string) => { throw new MeldImportError('Circular import detected', { code: 'CIRCULAR_IMPORT', details: { importChain: ['a.meld', 'b.meld', 'a.meld'] } }); }; try { // This should throw an error due to the circular import await context.services.interpreter.interpret([node], { filePath: 'test.meld' }); throw new Error('Should have thrown error'); } catch (error: unknown) { if (error instanceof MeldInterpreterError) { expect(error.message).toContain('Circular import'); } else { throw error; } } finally { // Restore original functionality context.services.circularity.beginImport = originalBeginImport; } }); }); describe('Complex scenarios', () => { it.todo('handles nested imports with state inheritance'); // V2: Complex state inheritance in nested imports requires improved state management it('maintains correct file paths during interpretation', async () => { // Create test context with files const ctx = new TestContext(); await ctx.initialize(); // Set up path variables in the state service ctx.services.state.setPathVar('PROJECTPATH', '/project'); ctx.services.state.setPathVar('HOMEPATH', '/home/user'); // Directly set the path variables we want to test ctx.services.state.setPathVar('mainPath', '/project/main.meld'); ctx.services.state.setPathVar('subPath', '/project/sub/sub.meld'); ctx.services.state.setPathVar('currentPath', '/project/sub/sub.meld'); ctx.services.state.setPathVar('relativePath', '/project/sub/relative.txt'); // Verify the paths are correctly maintained expect(ctx.services.state.getPathVar('mainPath')).toBeTruthy(); expect(ctx.services.state.getPathVar('subPath')).toBeTruthy(); expect(ctx.services.state.getPathVar('currentPath')).toBeTruthy(); expect(ctx.services.state.getPathVar('relativePath')).toBeTruthy(); // Check if the paths are correctly resolved const mainPath = ctx.services.state.getPathVar('mainPath'); const subPath = ctx.services.state.getPathVar('subPath'); const currentPath = ctx.services.state.getPathVar('currentPath'); const relativePath = ctx.services.state.getPathVar('relativePath'); // For structured paths, check the normalized value // Interface to type check path objects with normalized property interface NormalizedPath { normalized: string; } // Type guard function to check if a value is a NormalizedPath function isNormalizedPath(value: unknown): value is NormalizedPath { return value !== null && typeof value === 'object' && 'normalized' in value; } if (isNormalizedPath(mainPath)) { expect(mainPath.normalized).toBe('/project/main.meld'); } else { expect(mainPath).toBe('/project/main.meld'); } if (isNormalizedPath(subPath)) { expect(subPath.normalized).toBe('/project/sub/sub.meld'); } else { expect(subPath).toBe('/project/sub/sub.meld'); } if (isNormalizedPath(currentPath)) { expect(currentPath.normalized).toBe('/project/sub/sub.meld'); } else { expect(currentPath).toBe('/project/sub/sub.meld'); } if (isNormalizedPath(relativePath)) { expect(relativePath.normalized).toBe('/project/sub/relative.txt'); } else { expect(relativePath).toBe('/project/sub/relative.txt'); } }); it.todo('maintains correct state after successful imports'); // V2: State consistency across nested imports needs improved implementation }); describe('AST structure handling', () => { it('handles text directives with correct format', async () => { // MIGRATION: Using centralized syntax example instead of hardcoded directive const example = textDirectiveExamples.atomic.simpleString; const node = await createNodeFromExample(example.code); const result = await context.services.interpreter.interpret([node]); // Extract the variable name from the example const varName = node.directive.identifier; expect(result.getTextVar(varName)).toBeDefined(); expect(typeof result.getTextVar(varName)).toBe('string'); }); it('handles data directives with correct format', async () => { // MIGRATION: Using centralized syntax example instead of hardcoded directive const example = dataDirectiveExamples.atomic.simpleObject; const node = await createNodeFromExample(example.code); const result = await context.services.interpreter.interpret([node]); // Extract the variable name from the example const varName = node.directive.identifier; expect(result.getDataVar(varName)).toBeDefined(); expect(typeof result.getDataVar(varName)).toBe('object'); }); it('handles path directives with correct format', async () => { // MIGRATION NOTE: Using factory method directly due to issues with examples for simple paths // The create node from example approach doesn't work because the parser enforces path rules const node = context.factory.createPathDirective('test', 'filename.meld'); const result = await context.services.interpreter.interpret([node]); expect(result.getPathVar('test')).toBe('filename.meld'); }); it('handles complex directives with schema validation', async () => { // MIGRATION: Using centralized syntax example instead of hardcoded directive const example = dataDirectiveExamples.atomic.person; const node = await createNodeFromExample(example.code); const result = await context.services.interpreter.interpret([node]); // Extract the variable name from the example const varName = node.directive.identifier; const value = result.getDataVar(varName); expect(value).toBeDefined(); expect(typeof value).toBe('object'); }); it('maintains correct node order with mixed content', async () => { // MIGRATION: Using centralized examples instead of hardcoded directives const example1 = textDirectiveExamples.atomic.simpleString; const example2 = textDirectiveExamples.atomic.subject; const example3 = textDirectiveExamples.atomic.user; const node1 = await createNodeFromExample(example1.code); const node2 = await createNodeFromExample(example2.code); const node3 = await createNodeFromExample(example3.code); // Save the identifiers for later assertions const id1 = node1.directive.identifier; const id2 = node2.directive.identifier; const id3 = node3.directive.identifier; const result = await context.services.interpreter.interpret([node1, node2, node3]); const stateNodes = result.getNodes(); expect(stateNodes).toHaveLength(3); expect(stateNodes[0].type).toBe('Directive'); expect((stateNodes[0] as any).directive.identifier).toBe(id1); expect(stateNodes[1].type).toBe('Directive'); expect((stateNodes[1] as any).directive.identifier).toBe(id2); expect(stateNodes[2].type).toBe('Directive'); expect((stateNodes[2] as any).directive.identifier).toBe(id3); }); it.todo('handles nested directive values correctly'); // V2: Complex nested directive resolution requires enhanced variable scope handling }); });