meld
Version:
Meld: A template language for LLM prompts
301 lines (254 loc) • 10 kB
text/typescript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { DirectiveNode, MeldNode } from 'meld-spec';
import { EmbedDirectiveHandler } from '@services/pipeline/DirectiveService/handlers/execution/EmbedDirectiveHandler.js';
import { DirectiveError } from '@services/pipeline/DirectiveService/errors/DirectiveError.js';
import { MeldFileNotFoundError } from '@core/errors/MeldFileNotFoundError.js';
// Mock dependencies
const mockValidationService = {
validate: vi.fn()
};
const mockResolutionService = {
resolveInContext: vi.fn(),
extractSection: vi.fn()
};
const mockStateService = {
clone: vi.fn(),
createChildState: vi.fn(),
mergeChildState: vi.fn(),
isTransformationEnabled: vi.fn()
};
const mockCircularityService = {
beginImport: vi.fn(),
endImport: vi.fn(),
isInStack: vi.fn()
};
const mockFileSystemService = {
exists: vi.fn(),
readFile: vi.fn()
};
const mockParserService = {
parse: vi.fn()
};
const mockInterpreterService = {
interpret: vi.fn()
};
const mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
};
// Helper to create a directive node
function createEmbedDirectiveNode(path?: string): DirectiveNode {
return {
type: 'Directive',
directive: {
kind: 'embed',
path: path
},
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: 20 }
}
} as DirectiveNode;
}
describe('EmbedDirectiveHandler Fixes', () => {
let handler: EmbedDirectiveHandler;
let clonedState: any;
let childState: any;
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
// Setup mock states
childState = {
setTextVar: vi.fn(),
setDataVar: vi.fn(),
setPathVar: vi.fn(),
setCommand: vi.fn(),
getAllTextVars: vi.fn().mockReturnValue({ 'testVar': 'testValue' }),
getAllDataVars: vi.fn().mockReturnValue({ 'dataVar': { test: 'data' } }),
getAllPathVars: vi.fn().mockReturnValue({ 'pathVar': '/test/path' }),
getAllCommands: vi.fn().mockReturnValue({ 'cmdVar': 'echo test' }),
isTransformationEnabled: vi.fn().mockReturnValue(false)
};
clonedState = {
setTextVar: vi.fn(),
setDataVar: vi.fn(),
setPathVar: vi.fn(),
setCommand: vi.fn(),
createChildState: vi.fn().mockReturnValue(childState),
mergeChildState: vi.fn(),
isTransformationEnabled: vi.fn().mockReturnValue(false)
};
mockStateService.clone.mockReturnValue(clonedState);
mockStateService.createChildState.mockReturnValue(childState);
mockInterpreterService.interpret.mockResolvedValue(childState);
// Create handler
handler = new EmbedDirectiveHandler(
mockValidationService as any,
mockResolutionService as any,
mockStateService as any,
mockCircularityService as any,
mockFileSystemService as any,
mockParserService as any,
mockInterpreterService as any,
mockLogger as any
);
});
afterEach(() => {
vi.resetAllMocks();
});
describe('Error Handling', () => {
it('should throw DirectiveError when path is missing', async () => {
// Create a directive node without a path
const node = createEmbedDirectiveNode();
const context = { currentFilePath: 'test.meld', state: mockStateService };
// Execute and expect error
await expect(handler.execute(node, context)).rejects.toThrow(DirectiveError);
// The error is thrown directly, not logged
});
it('should throw DirectiveError when file does not exist', async () => {
// Create directive node with a path
const node = createEmbedDirectiveNode('missing.md');
const context = { currentFilePath: 'test.meld', state: mockStateService };
// Setup mock to say file doesn't exist
mockResolutionService.resolveInContext.mockResolvedValue('missing.md');
mockFileSystemService.exists.mockResolvedValue(false);
// Execute and expect error
await expect(handler.execute(node, context)).rejects.toThrow(DirectiveError);
// Verify service calls
expect(mockResolutionService.resolveInContext).toHaveBeenCalledWith('missing.md', expect.anything());
expect(mockFileSystemService.exists).toHaveBeenCalledWith('missing.md');
// File system services should never be called for reading
expect(mockFileSystemService.readFile).not.toHaveBeenCalled();
});
});
describe('File Existence Check', () => {
it('should check if file exists before reading', async () => {
// Create a directive node with a path
const node = createEmbedDirectiveNode('test.md');
const context = { currentFilePath: 'test.meld', state: mockStateService };
// Setup mocks
mockResolutionService.resolveInContext.mockResolvedValue('test.md');
mockFileSystemService.exists.mockResolvedValue(true);
mockFileSystemService.readFile.mockResolvedValue('Test content');
// Execute
await handler.execute(node, context);
// Verify that exists was called before readFile
expect(mockFileSystemService.exists).toHaveBeenCalledWith('test.md');
expect(mockFileSystemService.readFile).toHaveBeenCalledWith('test.md');
// Verify that parse is NOT called - embedded content should be treated as literal text
expect(mockParserService.parse).not.toHaveBeenCalled();
expect(mockInterpreterService.interpret).not.toHaveBeenCalled();
});
});
describe('Variable Reference Handling', () => {
it('should handle variable references without calling file system operations', async () => {
// Create a variable reference directive node
const variableNode = {
type: 'Directive',
directive: {
kind: 'embed',
path: {
raw: '{{role.architect}}',
isVariableReference: true,
variable: {
type: 'DataVar',
identifier: 'role',
fields: [{
type: 'field',
value: 'architect'
}]
}
}
},
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: 20 }
}
} as DirectiveNode;
const context = { currentFilePath: 'test.meld', state: mockStateService };
// Setup mocks
mockResolutionService.resolveInContext.mockResolvedValue(
'You are a senior architect skilled in assessing TypeScript codebases.'
);
// Execute
const result = await handler.execute(variableNode, context);
// Verify variable content was used directly
expect(mockResolutionService.resolveInContext).toHaveBeenCalled();
// Content should be treated as literal text - no parsing
expect(mockParserService.parse).not.toHaveBeenCalled();
// Verify file system was not used
expect(mockFileSystemService.exists).not.toHaveBeenCalled();
expect(mockFileSystemService.readFile).not.toHaveBeenCalled();
// Verify circularity service was not used
expect(mockCircularityService.beginImport).not.toHaveBeenCalled();
expect(mockCircularityService.endImport).not.toHaveBeenCalled();
// In transformation mode, we should get a replacement node
if (result.replacement) {
expect(result.replacement).toEqual({
type: 'Text',
content: 'You are a senior architect skilled in assessing TypeScript codebases.',
location: variableNode.location
});
}
});
it('should distinguish between variable references and file paths', async () => {
// Create a regular file path directive
const fileNode = createEmbedDirectiveNode('test.md');
// Create a variable reference directive
const variableNode = {
type: 'Directive',
directive: {
kind: 'embed',
path: {
raw: '{{content}}',
isVariableReference: true,
variable: {
type: 'TextVar',
identifier: 'content'
}
}
},
location: {
start: { line: 1, column: 1 },
end: { line: 1, column: 20 }
}
} as DirectiveNode;
const context = { currentFilePath: 'test.meld', state: mockStateService };
// Setup different resolutions based on path type
mockResolutionService.resolveInContext.mockImplementation((path) => {
if (typeof path === 'string') {
return Promise.resolve('test.md');
} else if (path?.isVariableReference) {
return Promise.resolve('Variable Content');
}
return Promise.resolve('');
});
mockFileSystemService.exists.mockResolvedValue(true);
mockFileSystemService.readFile.mockResolvedValue('File Content');
// Execute both directives
const fileResult = await handler.execute(fileNode, context);
const varResult = await handler.execute(variableNode, context);
// Verify different behavior for the two types
// File path: Should use file system
expect(mockFileSystemService.exists).toHaveBeenCalledTimes(1);
expect(mockFileSystemService.readFile).toHaveBeenCalledTimes(1);
expect(mockCircularityService.beginImport).toHaveBeenCalledTimes(1);
expect(mockCircularityService.endImport).toHaveBeenCalledTimes(1);
// Parser should NOT be called for either - content is treated as literal text
expect(mockParserService.parse).not.toHaveBeenCalled();
// Both should return replacement nodes with literal content
expect(fileResult.replacement).toEqual({
type: 'Text',
content: 'File Content',
location: fileNode.location
});
expect(varResult.replacement).toEqual({
type: 'Text',
content: 'Variable Content',
location: variableNode.location
});
});
});
});