UNPKG

meld

Version:

Meld: A template language for LLM prompts

245 lines (211 loc) 9.68 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { TextDirectiveHandler } from './TextDirectiveHandler.js'; import { createMockStateService, createMockValidationService, createMockResolutionService } from '@tests/utils/testFactories.js'; import type { DirectiveNode } from 'meld-spec'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import { IFileSystemService } from '@services/filesystem/FileSystemService/IFileSystemService.js'; /** * Helper function to create a directive node specifically with a @run command */ const createRunDirectiveNode = (identifier: string, command: string): DirectiveNode => { return { type: 'Directive', directive: { kind: 'text', identifier, value: `@run [${command}]`, source: 'run', run: { command: command } } }; }; /** * Helper function to create a standard text directive node */ const createTextDirectiveNode = (identifier: string, text: string): DirectiveNode => { return { type: 'Directive', directive: { kind: 'text', identifier, value: text } }; }; describe('TextDirectiveHandler - Command Execution', () => { let handler: TextDirectiveHandler; let stateService: ReturnType<typeof createMockStateService>; let validationService: ReturnType<typeof createMockValidationService>; let resolutionService: ReturnType<typeof createMockResolutionService>; let fileSystemService: IFileSystemService; let clonedState: IStateService; beforeEach(() => { // Create basic mock state services clonedState = { setTextVar: vi.fn(), getTextVar: vi.fn().mockImplementation((name: string) => { if (name === 'step1') return 'Command 1 output'; return undefined; }), getDataVar: vi.fn(), clone: vi.fn(), } as unknown as IStateService; stateService = { setTextVar: vi.fn(), getTextVar: vi.fn().mockImplementation((name: string) => { if (name === 'step1') return 'Command 1 output'; return undefined; }), getDataVar: vi.fn(), clone: vi.fn().mockReturnValue(clonedState) } as unknown as IStateService; validationService = createMockValidationService(); resolutionService = createMockResolutionService(); // Mock file system service for command execution fileSystemService = { executeCommand: vi.fn().mockImplementation((command: string) => { if (command.includes('echo "test"')) { return Promise.resolve({ stdout: 'test output', stderr: '', exitCode: 0 }); } if (command.includes('echo "Command 1 output"')) { return Promise.resolve({ stdout: 'Command 1 output', stderr: '', exitCode: 0 }); } if (command.includes('echo "Command 1 referenced')) { return Promise.resolve({ stdout: 'Command 1 referenced output', stderr: '', exitCode: 0 }); } if (command.includes('Output with')) { return Promise.resolve({ stdout: 'Output with \'single\' and "double" quotes', stderr: '', exitCode: 0 }); } if (command.includes('Line 1')) { return Promise.resolve({ stdout: 'Line 1\nLine 2\nLine 3', stderr: '', exitCode: 0 }); } return Promise.resolve({ stdout: 'generic output', stderr: '', exitCode: 0 }); }), readFile: vi.fn(), writeFile: vi.fn(), fileExists: vi.fn(), directoryExists: vi.fn(), getDirectoryContents: vi.fn(), getCwd: vi.fn().mockReturnValue('/Users/adam/dev/meld') }; // Create the handler with the mocked services handler = new TextDirectiveHandler(validationService, stateService, resolutionService); handler.setFileSystemService(fileSystemService); }); it('should execute command and store its output', async () => { // Arrange const node = createRunDirectiveNode('command_output', 'echo "test"'); const context = { state: stateService, currentFilePath: 'test.meld' }; // Act await handler.execute(node, context); // Assert expect(fileSystemService.executeCommand).toHaveBeenCalledWith('echo "test"', { cwd: '/Users/adam/dev/meld' }); expect(clonedState.setTextVar).toHaveBeenCalledWith('command_output', 'test output'); }); it('should handle variable references in command input', async () => { // Arrange const step1Node = createRunDirectiveNode('step1', 'echo "Command 1 output"'); // For the second node, we need to simulate the resolution of variables in the command const step2Node = createRunDirectiveNode('step2', 'echo "Command 1 referenced: {{step1}}"'); const context = { state: stateService, currentFilePath: 'test.meld', parentState: stateService }; // Act await handler.execute(step1Node, context); // Mock the resolutionService to handle the variable reference in the second command resolutionService.resolveInContext.mockImplementation((value) => { if (value === 'echo "Command 1 referenced: {{step1}}"') { return Promise.resolve('echo "Command 1 referenced: Command 1 output"'); } return Promise.resolve(value); }); await handler.execute(step2Node, context); // Assert expect(fileSystemService.executeCommand).toHaveBeenCalledTimes(2); expect(fileSystemService.executeCommand).toHaveBeenCalledWith('echo "Command 1 output"', { cwd: '/Users/adam/dev/meld' }); expect(fileSystemService.executeCommand).toHaveBeenCalledWith('echo "Command 1 referenced: Command 1 output"', { cwd: '/Users/adam/dev/meld' }); expect(clonedState.setTextVar).toHaveBeenCalledWith('step1', 'Command 1 output'); expect(clonedState.setTextVar).toHaveBeenCalledWith('step2', 'Command 1 referenced output'); }); it('should handle special characters in command outputs', async () => { // Arrange const node = createRunDirectiveNode('special', 'echo "Output with \'single\' and \\"double\\" quotes"'); const context = { state: stateService, currentFilePath: 'test.meld' }; // Act await handler.execute(node, context); // Assert expect(fileSystemService.executeCommand).toHaveBeenCalled(); expect(clonedState.setTextVar).toHaveBeenCalledWith('special', 'Output with \'single\' and "double" quotes'); }); it('should handle multi-line command outputs', async () => { // Arrange const node = createRunDirectiveNode('multiline', 'echo -e "Line 1\\nLine 2\\nLine 3"'); const context = { state: stateService, currentFilePath: 'test.meld' }; // Act await handler.execute(node, context); // Assert expect(fileSystemService.executeCommand).toHaveBeenCalled(); expect(clonedState.setTextVar).toHaveBeenCalledWith('multiline', 'Line 1\nLine 2\nLine 3'); }); it('should handle nested variable references across multiple levels', async () => { // Arrange - Create nodes for each level const level1Node = createRunDirectiveNode('level1', 'echo "Level 1 output"'); const level2Node = createRunDirectiveNode('level2', 'echo "Level 2 references {{level1}}"'); const level3Node = createRunDirectiveNode('level3', 'echo "Level 3 references {{level2}}"'); const context = { state: stateService, currentFilePath: 'test.meld', parentState: stateService }; // Mock the file system service to return appropriate outputs fileSystemService.executeCommand .mockImplementationOnce(() => Promise.resolve({ stdout: 'Level 1 output', stderr: '', exitCode: 0 })) .mockImplementationOnce(() => Promise.resolve({ stdout: 'Level 2 references Level 1 output', stderr: '', exitCode: 0 })) .mockImplementationOnce(() => Promise.resolve({ stdout: 'Level 3 references Level 2 references Level 1 output', stderr: '', exitCode: 0 })); // Mock the resolution service to handle variable resolution for each level resolutionService.resolveInContext .mockImplementationOnce(value => Promise.resolve(value)) // First command has no variables .mockImplementationOnce(value => { // For level2, replace {{level1}} with its output if (value === 'echo "Level 2 references {{level1}}"') { return Promise.resolve('echo "Level 2 references Level 1 output"'); } return Promise.resolve(value); }) .mockImplementationOnce(value => { // For level3, replace {{level2}} with its output if (value === 'echo "Level 3 references {{level2}}"') { return Promise.resolve('echo "Level 3 references Level 2 references Level 1 output"'); } return Promise.resolve(value); }); // Update mock state to return values for each level stateService.getTextVar = vi.fn().mockImplementation((name: string) => { if (name === 'level1') return 'Level 1 output'; if (name === 'level2') return 'Level 2 references Level 1 output'; return undefined; }); // Act - Execute each level in sequence await handler.execute(level1Node, context); await handler.execute(level2Node, context); await handler.execute(level3Node, context); // Assert expect(fileSystemService.executeCommand).toHaveBeenCalledTimes(3); expect(clonedState.setTextVar).toHaveBeenCalledWith('level1', 'Level 1 output'); expect(clonedState.setTextVar).toHaveBeenCalledWith('level2', 'Level 2 references Level 1 output'); expect(clonedState.setTextVar).toHaveBeenCalledWith('level3', 'Level 3 references Level 2 references Level 1 output'); }); });