meld
Version:
Meld: A template language for LLM prompts
244 lines (215 loc) • 8.67 kB
text/typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { OutputService } from '@services/pipeline/OutputService/OutputService.js';
import { DirectiveNode, TextNode, MeldNode } from 'meld-spec';
// Mock dependency for IStateService
const createMockState = (transformationEnabled = false, transformedNodes: MeldNode[] = []) => {
return {
getAllTextVars: vi.fn().mockReturnValue(new Map()),
getAllDataVars: vi.fn().mockReturnValue(new Map()),
getTextVar: vi.fn(),
getDataVar: vi.fn(),
isTransformationEnabled: vi.fn().mockReturnValue(transformationEnabled),
getTransformedNodes: vi.fn().mockReturnValue(transformedNodes),
transformNode: vi.fn(),
shouldTransform: vi.fn().mockReturnValue(true),
getTransformationOptions: vi.fn().mockReturnValue({})
};
};
// Mock dependency for IResolutionService
const createMockResolutionService = () => {
return {
resolveInContext: vi.fn(),
resolveText: vi.fn((text) => Promise.resolve(text)),
resolveDataVariable: vi.fn(),
resolvePathVariable: vi.fn(),
extractSection: vi.fn()
};
};
describe('OutputService Embed Transformation Bug Fix', () => {
let outputService: OutputService;
let mockState: any;
let mockResolutionService: any;
beforeEach(() => {
vi.resetAllMocks();
outputService = new OutputService();
mockResolutionService = createMockResolutionService();
mockState = createMockState();
outputService.initialize(mockState, mockResolutionService);
});
afterEach(() => {
vi.resetAllMocks();
});
describe('Embed Directive Processing', () => {
// Test case 1: Basic transformation should work
it('should replace embed directive with transformed content when exact line match exists', async () => {
// Setup mock state with transformation enabled
const transformedState = createMockState(true, [
{
type: 'Text',
content: 'Embedded content from file',
location: {
start: { line: 5, column: 1 },
end: { line: 5, column: 30 }
}
} as TextNode
]);
outputService.initialize(transformedState);
// Create a directive node that matches the transformed node's line number
const embedDirective: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'embed',
path: 'file.md'
},
location: {
start: { line: 5, column: 1 },
end: { line: 5, column: 20 }
}
};
// Convert the node to markdown with nodeToMarkdown to test just the directive handling
const result = await outputService.nodeToMarkdown(embedDirective, transformedState);
// Verify the output contains the embedded content, not a placeholder
expect(result.trim()).toBe('Embedded content from file');
expect(result).not.toContain('[directive output placeholder]');
});
// Test case 2: The bug scenario - line numbers don't match exactly
it('should replace embed directive with transformed content even when line numbers differ slightly', async () => {
// Setup transformed node with a DIFFERENT line number than the original directive
const transformedState = createMockState(true, [
{
type: 'Text',
content: 'Embedded content from file',
location: {
start: { line: 6, column: 1 }, // Note: line 6 instead of line 5
end: { line: 6, column: 30 }
}
} as TextNode
]);
outputService.initialize(transformedState);
// Create a directive node at line 5
const embedDirective: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'embed',
path: 'file.md'
},
location: {
start: { line: 5, column: 1 },
end: { line: 5, column: 20 }
}
};
// Test the nodeToMarkdown method directly
const result = await outputService.nodeToMarkdown(embedDirective, transformedState);
// With our fix, this should now find the closest node (line 6) even though it's not an exact match
expect(result.trim()).toBe('Embedded content from file');
expect(result).not.toContain('[directive output placeholder]');
});
// Test case 3: Multiple transformed nodes, should find the closest matching one
it('should find the closest transformed node when multiple exist', async () => {
// Setup multiple transformed nodes - these are what would be in the state's transformed nodes
const transformedState = createMockState(true, [
{
type: 'Text',
content: 'Wrong content from line 2',
location: {
start: { line: 2, column: 1 },
end: { line: 2, column: 30 }
}
} as TextNode,
{
type: 'Text',
content: 'Correct embedded content',
location: {
start: { line: 6, column: 1 }, // Close to original line 5
end: { line: 6, column: 30 }
}
} as TextNode,
{
type: 'Text',
content: 'Wrong content from line 10',
location: {
start: { line: 10, column: 1 },
end: { line: 10, column: 30 }
}
} as TextNode
]);
outputService.initialize(transformedState);
// Create a directive node at line 5 - this is what we'll process
// (in reality, we wouldn't process the Text nodes directly, they're just in the transformed nodes array)
const embedDirective: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'embed',
path: 'file.md'
},
location: {
start: { line: 5, column: 1 },
end: { line: 5, column: 20 }
}
};
// IMPORTANT: We only convert the directive node, not all the transformed nodes
// This is because in the real code, we iterate through the original nodes
// and look up their transformed versions
const result = await outputService.nodeToMarkdown(embedDirective, transformedState);
// Should match with the closest node (line 6)
expect(result.trim()).toBe('Correct embedded content');
expect(result).not.toContain('[directive output placeholder]');
expect(result).not.toContain('Wrong content from line 2');
expect(result).not.toContain('Wrong content from line 10');
});
// Test case 4: Run directive for comparison (currently works correctly)
it('should properly transform run directives (for comparison with embed)', async () => {
// Setup mock state with transformation enabled and a transformed run directive
const transformedState = createMockState(true, [
{
type: 'Text',
content: 'Output from run command',
location: {
start: { line: 5, column: 1 },
end: { line: 5, column: 30 }
}
} as TextNode
]);
outputService.initialize(transformedState);
// Create a run directive node
const runDirective: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'run',
command: 'echo "test"'
},
location: {
start: { line: 5, column: 1 },
end: { line: 5, column: 20 }
}
};
// Test the nodeToMarkdown method directly
const result = await outputService.nodeToMarkdown(runDirective, transformedState);
// Verify the output contains the command output, not a placeholder
expect(result.trim()).toBe('Output from run command');
expect(result).not.toContain('[run directive output placeholder]');
});
// Test case 5: For completeness - non-transformation mode still returns placeholder
it('should return placeholder in non-transformation mode', async () => {
// Setup mock state with transformation disabled
const nonTransformedState = createMockState(false);
outputService.initialize(nonTransformedState);
// Create a directive node
const embedDirective: DirectiveNode = {
type: 'Directive',
directive: {
kind: 'embed',
path: 'file.md'
},
location: {
start: { line: 5, column: 1 },
end: { line: 5, column: 20 }
}
};
// Test the nodeToMarkdown method directly
const result = await outputService.nodeToMarkdown(embedDirective, nonTransformedState);
// Verify the output contains the placeholder
expect(result.trim()).toBe('[directive output placeholder]');
});
});
});