meld
Version:
Meld: A template language for LLM prompts
295 lines (252 loc) • 9.88 kB
text/typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { DataResolver } from './DataResolver.js';
import { IStateService } from '@services/state/StateService/IStateService.js';
import { ResolutionContext } from '@services/resolution/ResolutionService/IResolutionService.js';
import { ResolutionError } from '@services/resolution/ResolutionService/errors/ResolutionError.js';
import { MeldNode } from 'meld-spec';
import { createTestText, createTestDirective } from '@tests/utils/nodeFactories.js';
import { ErrorSeverity } from '@core/errors/MeldError.js';
import { MeldResolutionError } from '@core/errors/MeldResolutionError.js';
import { MeldError } from '@core/errors/MeldError.js';
import {
ErrorCollector,
expectThrowsWithSeverity,
expectWarningsInPermissiveMode,
createStrictModeOptions,
createPermissiveModeOptions,
ErrorModeTestOptions
} from '@tests/utils/ErrorTestUtils.js';
/**
* Helper function to mimic the InterpreterService's error handling in permissive mode
*/
async function resolveWithPermissiveErrorHandling(
resolver: DataResolver,
node: MeldNode,
context: ResolutionContext,
errorHandler: (error: MeldError) => void
): Promise<string> {
try {
return await resolver.resolve(node, context);
} catch (error) {
if (error instanceof MeldError && error.severity === ErrorSeverity.Recoverable) {
// In permissive mode, handle recoverable errors
errorHandler(error);
return ''; // Return empty string for recoverable errors
}
// Re-throw fatal errors
throw error;
}
}
describe('DataResolver', () => {
let resolver: DataResolver;
let stateService: IStateService;
let context: ResolutionContext;
beforeEach(() => {
stateService = {
getDataVar: vi.fn(),
setDataVar: vi.fn(),
} as unknown as IStateService;
resolver = new DataResolver(stateService);
context = {
currentFilePath: 'test.meld',
allowedVariableTypes: {
text: true,
data: true,
path: true,
command: true
},
allowDataFields: true,
state: stateService
};
});
describe('resolve', () => {
it('should return content of text node unchanged', async () => {
const node = createTestText('test');
const result = await resolver.resolve(node, context);
expect(result).toBe('test');
});
it('should resolve data directive node', async () => {
const node = createTestDirective('data', 'data', 'value');
stateService.getDataVar.mockResolvedValue('value');
const result = await resolver.resolve(node, context);
expect(result).toBe('value');
expect(stateService.getDataVar).toHaveBeenCalledWith('data');
});
it('should convert objects to JSON strings', async () => {
const node = createTestDirective('data', 'data', '{ "test": "value" }');
stateService.getDataVar.mockResolvedValue({ test: 'value' });
const result = await resolver.resolve(node, context);
expect(result).toBe('{"test":"value"}');
expect(stateService.getDataVar).toHaveBeenCalledWith('data');
});
it('should handle null values', async () => {
const node = createTestDirective('data', 'data', 'null');
stateService.getDataVar.mockResolvedValue(null);
const result = await resolver.resolve(node, context);
expect(result).toBe('null');
expect(stateService.getDataVar).toHaveBeenCalledWith('data');
});
});
describe('error handling', () => {
it('should throw when data variables are not allowed', async () => {
context.allowedVariableTypes.data = false;
const node: MeldNode = {
type: 'Directive',
directive: {
kind: 'data',
identifier: 'test',
value: ''
}
};
await expect(resolver.resolve(node, context))
.rejects
.toThrow('Data variables are not allowed in this context');
});
it('should handle undefined variables appropriately', async () => {
// Arrange
stateService.getDataVar.mockResolvedValue(undefined);
const node = createTestDirective('data', 'undefined', '');
// Act & Assert - Strict mode
await expectThrowsWithSeverity(
() => resolver.resolve(node, { ...context, strict: true }),
MeldResolutionError,
ErrorSeverity.Recoverable
);
// Act & Assert - Permissive mode
const collector = new ErrorCollector();
// Use our wrapper function to mimic the InterpreterService's error handling
const result = await resolveWithPermissiveErrorHandling(
resolver,
node,
{ ...context, strict: false },
collector.handleError
);
// Should return empty string
expect(result).toBe('');
// Should have collected a warning
expect(collector.warnings.length).toBe(1);
expect(collector.warnings[0]).toBeInstanceOf(MeldResolutionError);
expect(collector.warnings[0].severity).toBe(ErrorSeverity.Recoverable);
});
it('should handle field access appropriately', async () => {
// Arrange
stateService.getDataVar.mockResolvedValue({ field: 'value' });
// Create a node with field access
const node = createTestDirective('data', 'data', '');
(node as any).directive.field = 'field';
// Act & Assert
// Test valid field access
const result = await resolver.resolve(node, context);
expect(result).toBe('value');
// Test non-existent field access in strict mode
(node as any).directive.field = 'nonexistent';
await expectThrowsWithSeverity(
() => resolver.resolve(node, { ...context, strict: true }),
MeldResolutionError,
ErrorSeverity.Recoverable
);
// Test non-existent field access in permissive mode
const collector = new ErrorCollector();
// Use our wrapper function to mimic the InterpreterService's error handling
const permissiveResult = await resolveWithPermissiveErrorHandling(
resolver,
node,
{ ...context, strict: false },
collector.handleError
);
// Should return empty string
expect(permissiveResult).toBe('');
// Should have collected a warning
expect(collector.warnings.length).toBe(1);
expect(collector.warnings[0]).toBeInstanceOf(MeldResolutionError);
expect(collector.warnings[0].severity).toBe(ErrorSeverity.Recoverable);
});
it('should handle null/undefined field access appropriately', async () => {
// Arrange
stateService.getDataVar.mockResolvedValue({
nullField: null,
undefinedField: undefined
});
// Test null field access
const node = createTestDirective('data', 'data', '');
(node as any).directive.field = 'nullField';
// Null fields should resolve to "null"
const nullResult = await resolver.resolve(node, context);
expect(nullResult).toBe('null');
// Test undefined field access
(node as any).directive.field = 'undefinedField';
await expectThrowsWithSeverity(
() => resolver.resolve(node, { ...context, strict: true }),
MeldResolutionError,
ErrorSeverity.Recoverable
);
// Test undefined field access in permissive mode
const collector = new ErrorCollector();
// Use our wrapper function to mimic the InterpreterService's error handling
const permissiveResult = await resolveWithPermissiveErrorHandling(
resolver,
node,
{ ...context, strict: false },
collector.handleError
);
// Should return empty string
expect(permissiveResult).toBe('');
// Should have collected a warning
expect(collector.warnings.length).toBe(1);
expect(collector.warnings[0]).toBeInstanceOf(MeldResolutionError);
expect(collector.warnings[0].severity).toBe(ErrorSeverity.Recoverable);
});
it('should handle accessing field of non-object', async () => {
// Arrange
stateService.getDataVar.mockResolvedValue({
stringField: 'string',
numberField: 42
});
// Test string field access
const node = createTestDirective('data', 'data', '');
(node as any).directive.field = 'stringField';
// Primitive values should be returned as strings
const stringResult = await resolver.resolve(node, context);
expect(stringResult).toBe('string');
// Test number field access
(node as any).directive.field = 'numberField';
// Numbers should be converted to strings
const numberResult = await resolver.resolve(node, context);
expect(numberResult).toBe('42');
});
});
describe('extractReferences', () => {
it('should extract variable identifier from data directive', async () => {
const node: MeldNode = {
type: 'Directive',
directive: {
kind: 'data',
identifier: 'test',
value: ''
}
};
const refs = resolver.extractReferences(node);
expect(refs).toEqual(['test']);
});
it('should return empty array for non-data directive', async () => {
const node: MeldNode = {
type: 'Directive',
directive: {
kind: 'text',
identifier: 'test',
value: ''
}
};
const refs = resolver.extractReferences(node);
expect(refs).toEqual([]);
});
it('should return empty array for text node', async () => {
const node: MeldNode = {
type: 'Text',
content: 'no references here'
};
const refs = resolver.extractReferences(node);
expect(refs).toEqual([]);
});
});
});