meld
Version:
Meld: A template language for LLM prompts
261 lines (220 loc) • 9.3 kB
text/typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { IStateService } from '@services/state/StateService/IStateService.js';
import { IStateTrackingService } from '@tests/utils/debug/StateTrackingService/IStateTrackingService.js';
import { StateVariableCopier } from './StateVariableCopier.js';
describe('StateVariableCopier', () => {
// Create mock state services
let sourceState: IStateService;
let targetState: IStateService;
let trackingService: IStateTrackingService;
let copier: StateVariableCopier;
beforeEach(() => {
// Mock source state
sourceState = {
getStateId: vi.fn().mockReturnValue('source-state-id'),
getCurrentFilePath: vi.fn().mockReturnValue('/path/to/file.meld'),
// Text variables
getAllTextVars: vi.fn().mockReturnValue(new Map([
['textVar1', 'text value 1'],
['textVar2', 'text value 2']
])),
getTextVar: vi.fn((name) => {
const vars = new Map([
['textVar1', 'text value 1'],
['textVar2', 'text value 2']
]);
return vars.get(name);
}),
// Data variables
getAllDataVars: vi.fn().mockReturnValue(new Map([
['dataVar1', { key: 'value' }],
['dataVar2', [1, 2, 3]]
])),
getDataVar: vi.fn((name) => {
const vars = new Map([
['dataVar1', { key: 'value' }],
['dataVar2', [1, 2, 3]]
]);
return vars.get(name);
}),
// Path variables
getAllPathVars: vi.fn().mockReturnValue(new Map([
['pathVar1', '/path/to/somewhere'],
['pathVar2', './relative/path']
])),
getPathVar: vi.fn((name) => {
const vars = new Map([
['pathVar1', '/path/to/somewhere'],
['pathVar2', './relative/path']
]);
return vars.get(name);
}),
// Commands
getAllCommands: vi.fn().mockReturnValue(new Map([
['cmd1', { command: 'echo hello' }],
['cmd2', { command: 'ls -la', options: { cwd: '/' } }]
])),
getCommand: vi.fn((name) => {
const commands = new Map([
['cmd1', { command: 'echo hello' }],
['cmd2', { command: 'ls -la', options: { cwd: '/' } }]
]);
return commands.get(name);
})
} as unknown as IStateService;
// Mock target state
targetState = {
getStateId: vi.fn().mockReturnValue('target-state-id'),
setTextVar: vi.fn(),
setDataVar: vi.fn(),
setPathVar: vi.fn(),
setCommand: vi.fn(),
getTextVar: vi.fn(),
getDataVar: vi.fn(),
getPathVar: vi.fn(),
getCommand: vi.fn()
} as unknown as IStateService;
// Mock tracking service
trackingService = {
trackContextBoundary: vi.fn(),
trackVariableCrossing: vi.fn()
} as unknown as IStateTrackingService;
// Create the copier with tracking
copier = new StateVariableCopier(trackingService);
});
describe('copyAllVariables', () => {
it('should copy all variables from source to target', () => {
// Act
const result = copier.copyAllVariables(sourceState, targetState);
// Assert
expect(result).toBe(8); // 2 text + 2 data + 2 path + 2 commands
// Verify text variables were copied
expect(targetState.setTextVar).toHaveBeenCalledWith('textVar1', 'text value 1');
expect(targetState.setTextVar).toHaveBeenCalledWith('textVar2', 'text value 2');
// Verify data variables were copied
expect(targetState.setDataVar).toHaveBeenCalledWith('dataVar1', { key: 'value' });
expect(targetState.setDataVar).toHaveBeenCalledWith('dataVar2', [1, 2, 3]);
// Verify path variables were copied
expect(targetState.setPathVar).toHaveBeenCalledWith('pathVar1', '/path/to/somewhere');
expect(targetState.setPathVar).toHaveBeenCalledWith('pathVar2', './relative/path');
// Verify commands were copied
expect(targetState.setCommand).toHaveBeenCalledWith('cmd1', { command: 'echo hello' });
expect(targetState.setCommand).toHaveBeenCalledWith('cmd2', { command: 'ls -la', options: { cwd: '/' } });
// Verify tracking was called
expect(trackingService.trackContextBoundary).toHaveBeenCalledTimes(2);
expect(trackingService.trackVariableCrossing).toHaveBeenCalledTimes(8);
});
it('should respect skipExisting option', () => {
// Setup: mock existing variables in target
targetState.getTextVar = vi.fn((name) => name === 'textVar1' ? 'existing value' : undefined);
targetState.getDataVar = vi.fn((name) => name === 'dataVar1' ? { existing: true } : undefined);
// Act
const result = copier.copyAllVariables(sourceState, targetState, { skipExisting: true });
// Assert: should copy 6 variables (skipping 2)
expect(result).toBe(6);
// textVar1 should not be copied because it exists
expect(targetState.setTextVar).not.toHaveBeenCalledWith('textVar1', expect.anything());
// textVar2 should be copied
expect(targetState.setTextVar).toHaveBeenCalledWith('textVar2', 'text value 2');
// dataVar1 should not be copied because it exists
expect(targetState.setDataVar).not.toHaveBeenCalledWith('dataVar1', expect.anything());
// dataVar2 should be copied
expect(targetState.setDataVar).toHaveBeenCalledWith('dataVar2', [1, 2, 3]);
});
it('should handle tracking options', () => {
// Act: disable tracking
copier.copyAllVariables(sourceState, targetState, {
trackContextBoundary: false,
trackVariableCrossing: false
});
// Assert: no tracking calls
expect(trackingService.trackContextBoundary).not.toHaveBeenCalled();
expect(trackingService.trackVariableCrossing).not.toHaveBeenCalled();
});
it('should handle missing methods gracefully', () => {
// Setup: remove some methods from the state
delete (sourceState as any).getAllTextVars;
delete (targetState as any).setDataVar;
// Act
const result = copier.copyAllVariables(sourceState, targetState);
// Assert: should only copy variables where methods exist
expect(result).toBe(4); // 0 text + 0 data + 2 path + 2 commands
// No text variables copied (missing getAllTextVars)
expect(targetState.setTextVar).not.toHaveBeenCalled();
// No data variables copied (missing setDataVar)
// Can't check not.toHaveBeenCalled since we deleted the method
// Just verify no errors were thrown
});
});
describe('copySpecificVariables', () => {
it('should copy specific variables by name', () => {
// Act
const result = copier.copySpecificVariables(
sourceState,
targetState,
[
{ name: 'textVar1' },
{ name: 'dataVar2' },
{ name: 'pathVar1', alias: 'customPath' }
]
);
// Assert
expect(result).toBe(3);
// Variables should be copied with correct names/aliases
expect(targetState.setTextVar).toHaveBeenCalledWith('textVar1', 'text value 1');
expect(targetState.setDataVar).toHaveBeenCalledWith('dataVar2', [1, 2, 3]);
expect(targetState.setPathVar).toHaveBeenCalledWith('customPath', '/path/to/somewhere');
// Tracking should include the alias
expect(trackingService.trackVariableCrossing).toHaveBeenCalledWith(
'source-state-id',
'target-state-id',
'pathVar1',
'path',
'customPath'
);
});
it('should skip non-existent variables', () => {
// Act
const result = copier.copySpecificVariables(
sourceState,
targetState,
[
{ name: 'textVar1' },
{ name: 'nonExistentVar' }
]
);
// Assert: only the existing variable should be copied
expect(result).toBe(1);
expect(targetState.setTextVar).toHaveBeenCalledWith('textVar1', 'text value 1');
});
});
describe('Error handling', () => {
it('should handle errors when getting variables', () => {
// Setup: make getAllTextVars throw
sourceState.getAllTextVars = vi.fn().mockImplementation(() => {
throw new Error('Test error');
});
// Act
const result = copier.copyAllVariables(sourceState, targetState);
// Assert: should continue with other variable types
expect(result).toBe(6); // 0 text + 2 data + 2 path + 2 commands
});
it('should handle errors when getting file path', () => {
// Setup: make getCurrentFilePath throw
sourceState.getCurrentFilePath = vi.fn().mockImplementation(() => {
throw new Error('Test error');
});
// Act
const result = copier.copyAllVariables(sourceState, targetState);
// Assert: should still copy variables
expect(result).toBe(8);
// Context boundary should be tracked without file path
expect(trackingService.trackContextBoundary).toHaveBeenCalledWith(
'source-state-id',
'target-state-id',
'import',
undefined
);
});
});
});