meld
Version:
Meld: A template language for LLM prompts
511 lines (434 loc) • 21.7 kB
text/typescript
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
});
});