meld
Version:
Meld: A template language for LLM prompts
245 lines (211 loc) • 9.68 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 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');
});
});