meld
Version:
Meld: A template language for LLM prompts
223 lines (188 loc) • 7.71 kB
text/typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TextDirectiveHandler } from './TextDirectiveHandler.js';
import { createMockStateService, createMockValidationService, createMockResolutionService } from '@tests/utils/testFactories.js';
import { DirectiveError, DirectiveErrorCode } from '@services/pipeline/DirectiveService/errors/DirectiveError.js';
import { ResolutionError } from '@services/resolution/ResolutionService/errors/ResolutionError.js';
import { ResolutionErrorCode } from '@services/resolution/ResolutionService/IResolutionService.js';
import type { DirectiveNode } from 'meld-spec';
import type { IStateService } from '@services/state/StateService/IStateService.js';
import { ErrorCollector } from '@tests/utils/ErrorTestUtils.js';
import { ErrorSeverity } from '@core/errors/MeldError.js';
describe('TextDirectiveHandler Integration', () => {
let handler: TextDirectiveHandler;
let stateService: ReturnType<typeof createMockStateService>;
let validationService: ReturnType<typeof createMockValidationService>;
let resolutionService: ReturnType<typeof createMockResolutionService>;
let clonedState: IStateService;
beforeEach(() => {
clonedState = {
setTextVar: vi.fn(),
getTextVar: vi.fn(),
getDataVar: vi.fn(),
clone: vi.fn(),
} as unknown as IStateService;
stateService = {
setTextVar: vi.fn(),
getTextVar: vi.fn(),
getDataVar: vi.fn(),
clone: vi.fn().mockReturnValue(clonedState)
} as unknown as IStateService;
validationService = createMockValidationService();
resolutionService = createMockResolutionService();
handler = new TextDirectiveHandler(validationService, stateService, resolutionService);
});
describe('complex scenarios', () => {
it('should handle nested variable references', async () => {
const node: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'text',
identifier: 'greeting',
value: 'Hello {{user.{{type}}.name}}!'
}
};
const context = {
state: stateService,
currentFilePath: 'test.meld'
};
vi.mocked(resolutionService.resolveInContext)
.mockResolvedValue('Hello Alice!');
const result = await handler.execute(node, context);
expect(clonedState.setTextVar).toHaveBeenCalledWith('greeting', 'Hello Alice!');
});
it('should handle mixed string literals and variables', async () => {
const node: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'text',
identifier: 'message',
value: '{{prefix}} "quoted {{name}}" {{suffix}}'
}
};
const context = {
state: stateService,
currentFilePath: 'test.meld'
};
vi.mocked(resolutionService.resolveInContext)
.mockResolvedValue('Hello "quoted World" !');
const result = await handler.execute(node, context);
expect(clonedState.setTextVar).toHaveBeenCalledWith('message', 'Hello "quoted World" !');
});
it('should handle complex data structure access', async () => {
const node: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'text',
identifier: 'userInfo',
value: '{{user.contacts[{{index}}].email}}'
}
};
const context = {
state: stateService,
currentFilePath: 'test.meld'
};
vi.mocked(resolutionService.resolveInContext)
.mockResolvedValue('second@example.com');
const result = await handler.execute(node, context);
expect(clonedState.setTextVar).toHaveBeenCalledWith('userInfo', 'second@example.com');
});
it('should handle environment variables with fallbacks', async () => {
const node: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'text',
identifier: 'config',
value: '{{ENV_HOST:-localhost}}:{{ENV_PORT:-3000}}'
}
};
const context = {
state: stateService,
currentFilePath: 'test.meld'
};
process.env.ENV_HOST = 'example.com';
// ENV_PORT not set, should use fallback
vi.mocked(resolutionService.resolveInContext)
.mockResolvedValue('example.com:3000');
const result = await handler.execute(node, context);
expect(clonedState.setTextVar).toHaveBeenCalledWith('config', 'example.com:3000');
delete process.env.ENV_HOST;
});
it.todo('should handle circular reference detection - Complex error handling deferred for V1');
it.todo('should handle error propagation through the stack - Complex error propagation deferred for V1');
it('should handle validation errors with proper context', async () => {
const node: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'text',
identifier: 'invalid',
value: null // Invalid value - should be a string
},
location: {
start: { line: 5, column: 1 },
end: { line: 5, column: 25 }
}
};
const context = {
state: stateService,
currentFilePath: 'test.meld'
};
// Mock validation service to throw a DirectiveError
validationService.validate = vi.fn().mockImplementation(() => {
// Create an error with location and other properties set correctly
const error = new DirectiveError(
'Invalid text directive value',
'text',
DirectiveErrorCode.VALIDATION_FAILED,
{
node,
context: {
...context,
filePath: 'test.meld'
},
location: {
...node.location,
start: {
...node.location.start,
line: 5,
column: 1
}
}
}
);
// Set the severity property directly on the error
// @ts-ignore - We know this will work
error.severity = ErrorSeverity.Fatal;
throw error;
});
// Use ErrorCollector to test both strict and permissive modes
const errorCollector = new ErrorCollector();
// Test strict mode (should throw)
await expect(async () => {
try {
await handler.execute(node, context);
} catch (error) {
// Ensure we're passing an appropriate error to the handler
if (error instanceof DirectiveError) {
errorCollector.handleError(error);
}
throw error;
}
}).rejects.toThrow(DirectiveError);
// Verify error was collected with correct severity
expect(errorCollector.getAllErrors()).toHaveLength(1);
expect(errorCollector.getAllErrors()[0].severity).toBe(ErrorSeverity.Fatal);
expect(errorCollector.getAllErrors()[0].message).toContain('Invalid text directive value');
// Verify error contains location information
const error = errorCollector.getAllErrors()[0];
expect(error.context).toBeDefined();
// With the refactored code, the location is now in a different structure
// The DirectiveError wraps the location in the context.node.location
expect(error.context.node).toBeDefined();
expect(error.context.node.location).toBeDefined();
expect(error.context.node.location.start.line).toBe(5);
expect(error.context.context).toBeDefined();
expect(error.context.context.currentFilePath).toBe('test.meld');
});
it.todo('should handle mixed directive types - Complex directive interaction deferred for V1');
});
});