UNPKG

meld

Version:

Meld: A template language for LLM prompts

457 lines (387 loc) 15.4 kB
// Mock the logger before any imports const mockLogger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; vi.mock('../../../../core/utils/logger', () => ({ directiveLogger: mockLogger })); import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { RunDirectiveHandler } from './RunDirectiveHandler.js'; import type { DirectiveNode, MeldNode } from 'meld-spec'; import type { IValidationService } from '@services/resolution/ValidationService/IValidationService.js'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import type { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js'; import type { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js'; import { DirectiveError, DirectiveErrorCode } from '@services/pipeline/DirectiveService/errors/DirectiveError.js'; import { exec } from 'child_process'; import { promisify } from 'util'; // Import the centralized syntax examples and helpers but don't use the problematic syntax-test-helpers import { runDirectiveExamples } from '@core/syntax/index.js'; import { parse } from 'meld-ast'; import { ErrorSeverity } from '@core/errors'; // Mock child_process vi.mock('child_process', () => ({ exec: vi.fn() })); /** * RunDirectiveHandler Test Migration Status * ---------------------------------------- * * MIGRATION STATUS: Completed * * This test file has been updated to no longer rely on syntax-test-helpers. * It now uses direct syntax examples that match the expected syntax in the codebase. */ // Direct usage of meld-ast instead of mock factories const createRealParserService = () => { // Create the parse function const parseFunction = async (content: string): Promise<MeldNode[]> => { // Use the real meld-ast parser with dynamic import try { return await parse(content, { trackLocations: true, validateNodes: true }); } catch (error) { console.error('Parser error:', error); throw error; } }; return { parse: parseFunction }; }; // Helper to create a DirectiveNode directly from example code async function createDirectiveNode(code: string): Promise<DirectiveNode> { try { const result = await parse(code, { trackLocations: true, validateNodes: true }); // The parse function might return an AST property const nodes = Array.isArray(result) ? result : (result.ast || []); if (nodes.length === 0 || nodes[0].type !== 'Directive') { throw new Error(`Failed to parse directive from code: ${code}`); } return nodes[0] as DirectiveNode; } catch (error) { console.error('Error creating directive node:', error); throw error; } } // Helper to create a run directive node directly without parsing const createRunDirectiveNode = (command: string, outputVar?: string): DirectiveNode => { return { type: 'Directive', directive: { kind: 'run', command, output: outputVar } } as DirectiveNode; }; // Migration Status: In progress - updating to use centralized syntax examples // TODO: Convert more tests to use centralized examples // Helper function to create parser services for testing function createServices() { const validationService = { validate: vi.fn() }; const resolutionService = { resolveInContext: vi.fn() }; const fileSystemService = { executeCommand: vi.fn(), getWorkspacePath: vi.fn().mockReturnValue('/workspace') }; return { validationService, resolutionService, fileSystemService }; } describe('RunDirectiveHandler', () => { let handler: RunDirectiveHandler; let validationService: IValidationService; let stateService: IStateService; let resolutionService: IResolutionService; let fileSystemService: IFileSystemService; let clonedState: IStateService; beforeEach(() => { validationService = { validate: vi.fn(), registerValidator: vi.fn(), removeValidator: vi.fn(), hasValidator: vi.fn(), getRegisteredDirectiveKinds: vi.fn() } as unknown as IValidationService; clonedState = { setTextVar: vi.fn(), clone: vi.fn(), isTransformationEnabled: vi.fn().mockReturnValue(false), transformNode: vi.fn() } as unknown as IStateService; stateService = { setTextVar: vi.fn(), clone: vi.fn().mockReturnValue(clonedState), isTransformationEnabled: vi.fn().mockReturnValue(false) } as unknown as IStateService; resolutionService = { resolveInContext: vi.fn() } as unknown as IResolutionService; fileSystemService = { getCwd: vi.fn().mockReturnValue('/workspace'), executeCommand: vi.fn(), dirname: vi.fn().mockReturnValue('/workspace'), join: vi.fn().mockImplementation((...args) => args.join('/')), normalize: vi.fn().mockImplementation(path => path) } as unknown as IFileSystemService; handler = new RunDirectiveHandler( validationService, resolutionService, stateService, fileSystemService ); // Reset mocks vi.clearAllMocks(); }); describe('basic command execution', () => { it('should execute simple commands', async () => { // Create node directly without relying on the parser const node = createRunDirectiveNode('echo test'); const context = { currentFilePath: 'test.meld', state: stateService, workingDirectory: '/workspace' }; // Mock the command execution response vi.mocked(fileSystemService.executeCommand).mockResolvedValue({ stdout: 'command output', stderr: '', exitCode: 0 }); // We need to mock this differently, string commands are handled differently vi.mocked(resolutionService.resolveInContext).mockResolvedValue('echo test'); // Execute the directive await handler.execute(node, context); // Verify that the command was executed correctly expect(fileSystemService.executeCommand).toHaveBeenCalledWith('echo test', { cwd: '/workspace' }); }); it('should handle commands with variables', async () => { // Create a directive node directly without parsing const node = createRunDirectiveNode('echo {{greeting}} {{name}}'); const context = { currentFilePath: 'test.meld', state: stateService, workingDirectory: '/workspace' }; // Mock variable resolution - the actual handler calls resolveInContext directly with the command string vi.mocked(resolutionService.resolveInContext).mockResolvedValue('echo Hello World'); // Mock command execution vi.mocked(fileSystemService.executeCommand).mockResolvedValue({ stdout: 'Hello World', stderr: '', exitCode: 0 }); // Execute the directive await handler.execute(node, context); // Verify the command was executed with resolved variables expect(fileSystemService.executeCommand).toHaveBeenCalledWith('echo Hello World', { cwd: '/workspace' }); }); it('should handle custom output variable', async () => { // Create a node that captures output to a variable const node = createRunDirectiveNode('echo test', 'variable_name'); const context = { currentFilePath: 'test.meld', state: stateService, workingDirectory: '/workspace' }; // Mock variable resolution and command execution vi.mocked(resolutionService.resolveInContext).mockResolvedValue('echo test'); vi.mocked(fileSystemService.executeCommand).mockResolvedValue({ stdout: 'command output', stderr: '', exitCode: 0 }); // Execute the directive await handler.execute(node, context); // Verify the output was captured in the variable expect(clonedState.setTextVar).toHaveBeenCalledWith('variable_name', 'command output'); }); it('should handle commands with variables', async () => { // Create node directly with the correct syntax const node = await createDirectiveNode('@run [echo {{greeting}}, {{name}}!]'); const context = { currentFilePath: 'test.meld', state: stateService }; const clonedState = { ...stateService, clone: vi.fn().mockReturnThis(), setTextVar: vi.fn(), getTextVar: vi.fn(), isTransformationEnabled: vi.fn().mockReturnValue(false) }; // Setup mocks to return values for greeting and name clonedState.getTextVar.mockImplementation((key) => { if (key === 'greeting') return 'Hello'; if (key === 'name') return 'World'; return undefined; }); vi.mocked(stateService.clone).mockReturnValue(clonedState); vi.mocked(validationService.validate).mockResolvedValue(undefined); vi.mocked(resolutionService.resolveInContext).mockResolvedValue('echo Hello, World!'); vi.mocked(fileSystemService.executeCommand).mockResolvedValue({ stdout: 'test output', stderr: '' }); const result = await handler.execute(node, context); // The handler should be using the cloned state, not the original context expect(resolutionService.resolveInContext).toHaveBeenCalled(); // Just verify that the command is executed correctly expect(fileSystemService.executeCommand).toHaveBeenCalledWith( 'echo Hello, World!', expect.objectContaining({ cwd: '/workspace' }) ); }); it('should handle custom output variable', async () => { // Instead of using createRealRunDirective, create a node directly // This bypasses the createRealRunDirective function which is using problematic syntax const node = { type: 'Directive', directive: { kind: 'run', identifier: 'run', command: 'echo test', output: 'variable_name' }, location: { start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 20, offset: 19 } } } as DirectiveNode; const context = { currentFilePath: 'test.meld', state: stateService }; const clonedState = { ...stateService, clone: vi.fn().mockReturnThis(), setTextVar: vi.fn(), isTransformationEnabled: vi.fn().mockReturnValue(false) }; vi.mocked(stateService.clone).mockReturnValue(clonedState); vi.mocked(validationService.validate).mockResolvedValue(undefined); vi.mocked(resolutionService.resolveInContext).mockResolvedValue('echo test'); vi.mocked(fileSystemService.executeCommand).mockResolvedValue({ stdout: 'test output', stderr: '' }); const result = await handler.execute(node, context); expect(fileSystemService.executeCommand).toHaveBeenCalledWith( 'echo test', expect.objectContaining({ cwd: '/workspace' }) ); expect(clonedState.setTextVar).toHaveBeenCalledWith('variable_name', 'test output'); expect(result.state).toBe(clonedState); }); }); describe('error handling', () => { it('should handle validation errors', async () => { // Create an empty run command that would trigger validation errors const node = createRunDirectiveNode(''); const context = { currentFilePath: 'test.meld', state: stateService, workingDirectory: '/workspace' }; vi.mocked(validationService.validate).mockRejectedValue( new DirectiveError( 'Invalid command', DirectiveErrorCode.InvalidCommand, ErrorSeverity.Error ) ); // Verify the error is properly thrown and handled await expect(handler.execute(node, context)).rejects.toThrow(DirectiveError); }); it('should handle resolution errors', async () => { // Create a run command with an undefined variable const node = createRunDirectiveNode('{{undefined_var}}'); const context = { currentFilePath: 'test.meld', state: stateService, workingDirectory: '/workspace' }; vi.mocked(validationService.validate).mockResolvedValue(undefined); vi.mocked(resolutionService.resolveInContext).mockRejectedValue( new Error('Variable not found') ); // Verify the error is properly thrown and handled await expect(handler.execute(node, context)).rejects.toThrow(); }); it('should handle command execution errors', async () => { // Create a node for a command that will fail const node = createRunDirectiveNode('invalid-command'); const context = { currentFilePath: 'test.meld', state: stateService, workingDirectory: '/workspace' }; vi.mocked(validationService.validate).mockResolvedValue(undefined); vi.mocked(resolutionService.resolveInContext).mockResolvedValue('invalid-command'); vi.mocked(fileSystemService.executeCommand).mockRejectedValue( new Error('Command failed') ); // Verify the error is properly thrown and handled await expect(handler.execute(node, context)).rejects.toThrow(); expect(fileSystemService.executeCommand).toHaveBeenCalledWith('invalid-command', { cwd: '/workspace' }); }); }); describe('output handling', () => { it('should handle stdout and stderr', async () => { const node = createRunDirectiveNode('echo error >&2'); const context = { currentFilePath: 'test.meld', state: stateService, workingDirectory: '/workspace' }; vi.mocked(validationService.validate).mockResolvedValue(undefined); vi.mocked(resolutionService.resolveInContext).mockResolvedValue('echo error >&2'); vi.mocked(fileSystemService.executeCommand).mockResolvedValue({ stdout: '', stderr: 'error message', exitCode: 0 }); await handler.execute(node, context); expect(clonedState.setTextVar).toHaveBeenCalledWith('stdout', ''); expect(clonedState.setTextVar).toHaveBeenCalledWith('stderr', 'error message'); }); it('should handle transformation mode', async () => { const node = createRunDirectiveNode('echo test'); const context = { currentFilePath: 'test.meld', state: stateService, workingDirectory: '/workspace' }; vi.mocked(validationService.validate).mockResolvedValue(undefined); vi.mocked(resolutionService.resolveInContext).mockResolvedValue('echo test'); vi.mocked(fileSystemService.executeCommand).mockResolvedValue({ stdout: 'transformed output', stderr: '', exitCode: 0 }); // In transformation mode, the result should contain the output vi.mocked(stateService.isTransformationEnabled).mockReturnValue(false); vi.mocked(clonedState.isTransformationEnabled).mockReturnValue(true); const result = await handler.execute(node, context); expect(result.replacement).toEqual(expect.objectContaining({ type: 'Text', content: 'transformed output' })); }); }); });