UNPKG

meld

Version:

Meld: A template language for LLM prompts

191 lines (163 loc) 7.86 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { VariableReferenceResolver } from './VariableReferenceResolver.js'; import { createMockStateService, createMockParserService, createTextNode, createDirectiveNode } from '@tests/utils/testFactories.js'; import { ResolutionError } from '@services/resolution/ResolutionService/errors/ResolutionError.js'; import type { ResolutionContext, ResolutionErrorCode } from '@services/resolution/ResolutionService/IResolutionService.js'; import type { MeldNode, TextNode, DirectiveNode } from 'meld-spec'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import type { IParserService } from '@services/pipeline/ParserService/IParserService.js'; describe('VariableReferenceResolver', () => { let resolver: VariableReferenceResolver; let stateService: ReturnType<typeof createMockStateService>; let parserService: ReturnType<typeof createMockParserService>; let context: ResolutionContext; beforeEach(() => { stateService = createMockStateService(); parserService = createMockParserService(); resolver = new VariableReferenceResolver(stateService, undefined, parserService); context = { allowedVariableTypes: { text: true, data: true, path: true, command: true }, currentFilePath: 'test.meld', state: stateService }; }); describe('resolve', () => { it('should resolve text variables', async () => { vi.mocked(stateService.getTextVar).mockReturnValue('Hello World'); const result = await resolver.resolve('{{greeting}}', context); expect(result).toBe('Hello World'); expect(stateService.getTextVar).toHaveBeenCalledWith('greeting'); }); it('should resolve data variables when text variable not found', async () => { vi.mocked(parserService.parse).mockResolvedValue([ createDirectiveNode('data', { identifier: 'data', value: 'Data Value' }) ]); vi.mocked(stateService.getTextVar).mockReturnValue(undefined); vi.mocked(stateService.getDataVar).mockReturnValue('Data Value'); const result = await resolver.resolve('{{data}}', context); expect(result).toBe('Data Value'); expect(stateService.getTextVar).toHaveBeenCalledWith('data'); expect(stateService.getDataVar).toHaveBeenCalledWith('data'); }); it('should handle multiple variable references', async () => { vi.mocked(parserService.parse).mockResolvedValue([ createTextNode(''), createDirectiveNode('text', { identifier: 'greeting1', value: 'Hello' }), createTextNode(' '), createDirectiveNode('text', { identifier: 'greeting2', value: 'World' }), createTextNode('!') ]); vi.mocked(stateService.getTextVar) .mockReturnValueOnce('Hello') .mockReturnValueOnce('World'); const result = await resolver.resolve('{{greeting1}} {{greeting2}}!', context); expect(result).toBe('Hello World!'); }); it('should handle field access in data variables', async () => { vi.mocked(parserService.parse).mockResolvedValue([ createDirectiveNode('data', { identifier: 'data', fields: ['user', 'name'] }) ]); vi.mocked(stateService.getTextVar).mockReturnValue(undefined); vi.mocked(stateService.getDataVar).mockReturnValue({ user: { name: 'Alice' } }); const result = await resolver.resolve('{{data.user.name}}', context); expect(result).toBe('Alice'); }); it('should handle environment variables', async () => { vi.mocked(parserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'ENV_TEST' }) ]); vi.mocked(stateService.getTextVar).mockReturnValue(undefined); vi.mocked(stateService.getDataVar).mockReturnValue(undefined); await expect(resolver.resolve('{{ENV_TEST}}', context)) .rejects .toThrow('Variable ENV_TEST not found'); }); it('should throw for undefined variables', async () => { vi.mocked(parserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'missing' }) ]); vi.mocked(stateService.getTextVar).mockReturnValue(undefined); vi.mocked(stateService.getDataVar).mockReturnValue(undefined); await expect(resolver.resolve('{{missing}}', context)) .rejects .toThrow('Variable missing not found'); }); it('should preserve text without variables', async () => { vi.mocked(parserService.parse).mockResolvedValue([ createTextNode('No variables here') ]); const result = await resolver.resolve('No variables here', context); expect(result).toBe('No variables here'); expect(stateService.getTextVar).not.toHaveBeenCalled(); }); it('should handle mixed content with variables', async () => { vi.mocked(parserService.parse).mockResolvedValue([ createTextNode('Hello '), createDirectiveNode('text', { identifier: 'name', value: 'Alice' }), createTextNode(', welcome to '), createDirectiveNode('text', { identifier: 'place', value: 'Wonderland' }), createTextNode('!') ]); vi.mocked(stateService.getTextVar) .mockReturnValueOnce('Alice') .mockReturnValueOnce('Wonderland'); const result = await resolver.resolve( 'Hello {{name}}, welcome to {{place}}!', context ); expect(result).toBe('Hello Alice, welcome to Wonderland!'); }); it('should fall back to regex resolution when parser fails', async () => { vi.mocked(parserService.parse).mockRejectedValue(new Error('Parser error')); vi.mocked(stateService.getTextVar).mockReturnValue('Fallback Value'); const result = await resolver.resolve('{{fallback}}', context); expect(result).toBe('Fallback Value'); expect(stateService.getTextVar).toHaveBeenCalledWith('fallback'); }); }); describe('extractReferences', () => { it('should extract all variable references', async () => { const refs = resolver.extractReferences('{{var1}} and {{var2}} and {{var3}}'); expect(refs).toEqual(['var1', 'var2', 'var3']); }); it('should handle field access in references', async () => { const refs = resolver.extractReferences('{{data.field1}} and {{data.field2}}'); expect(refs).toEqual(['data']); }); it('should return empty array for no references', async () => { const refs = resolver.extractReferences('No variables here'); expect(refs).toEqual([]); }); it('should handle duplicate references', async () => { const refs = resolver.extractReferences('{{var1}} and {{var1}} and {{var1}}'); expect(refs).toEqual(['var1']); }); }); describe('extractReferencesAsync', () => { it('should extract all variable references using AST when available', async () => { vi.mocked(parserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'var1' }), createTextNode(' and '), createDirectiveNode('text', { identifier: 'var2' }), createTextNode(' and '), createDirectiveNode('text', { identifier: 'var3' }) ]); const refs = await resolver.extractReferencesAsync('{{var1}} and {{var2}} and {{var3}}'); expect(refs).toEqual(['var1', 'var2', 'var3']); expect(parserService.parse).toHaveBeenCalled(); }); it('should fall back to regex when parser fails', async () => { vi.mocked(parserService.parse).mockRejectedValue(new Error('Parser error')); const refs = await resolver.extractReferencesAsync('{{var1}} and {{var2}}'); expect(refs).toEqual(['var1', 'var2']); }); }); });