UNPKG

meld

Version:

Meld: A template language for LLM prompts

289 lines (250 loc) 10.6 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { StringConcatenationHandler } from './StringConcatenationHandler.js'; import { ResolutionError } from '@services/resolution/ResolutionService/errors/ResolutionError.js'; import { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js'; import { ResolutionContext, ResolutionErrorCode } from '@services/resolution/ResolutionService/IResolutionService.js'; import { createMockParserService, createDirectiveNode, createTextNode } from '@tests/utils/testFactories.js'; import type { IParserService } from '@services/pipeline/ParserService/IParserService.js'; describe('StringConcatenationHandler', () => { let handler: StringConcatenationHandler; let mockResolutionService: IResolutionService; let mockParserService: ReturnType<typeof createMockParserService>; let context: ResolutionContext; beforeEach(() => { mockResolutionService = { resolveInContext: vi.fn() } as unknown as IResolutionService; mockParserService = createMockParserService(); handler = new StringConcatenationHandler(mockResolutionService, mockParserService); context = { currentFilePath: 'test.meld', allowedVariableTypes: { text: true, data: true, path: true, command: true }, state: {} as any }; }); describe('hasConcatenation', () => { it('should detect valid concatenation operators using AST', async () => { // Mock the AST for concatenation syntax vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ['"hello"', '"world"'] } }) ]); expect(await handler.hasConcatenation('"hello" ++ "world"')).toBe(true); expect(mockParserService.parse).toHaveBeenCalled(); }); it('should fall back to regex detection when AST parsing fails', async () => { // Mock parser to throw an error vi.mocked(mockParserService.parse).mockRejectedValue(new Error('Parse error')); expect(await handler.hasConcatenation('"hello" ++ "world"')).toBe(true); expect(await handler.hasConcatenation('"hello"++"world"')).toBe(false); // No spaces }); it('should reject invalid concatenation operators', async () => { // Mock the AST without concatenation vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: '"hello"' }) ]); expect(await handler.hasConcatenation('"hello" + + "world"')).toBe(false); // Split ++ expect(await handler.hasConcatenation('"hello" + "world"')).toBe(false); // Single + }); }); describe('resolveConcatenation', () => { it('should use AST parsing to split concatenation parts', async () => { // Mock the AST for concatenation vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ['"hello"', '" "', '"world"'], raw: '"hello" ++ " " ++ "world"' } }) ]); // Mock resolution service vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === '"hello"') return 'hello'; if (value === '" "') return ' '; if (value === '"world"') return 'world'; return String(value); }); const result = await handler.resolveConcatenation('"hello" ++ " " ++ "world"', context); expect(result).toBe('hello world'); expect(mockParserService.parse).toHaveBeenCalled(); }); it('should fall back to regex-based splitting when AST parsing fails', async () => { // Mock parser to throw an error vi.mocked(mockParserService.parse).mockRejectedValue(new Error('Parse error')); // Mock resolution service vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === '"hello"') return 'hello'; if (value === '" "') return ' '; if (value === '"world"') return 'world'; return String(value); }); const result = await handler.resolveConcatenation('"hello" ++ " " ++ "world"', context); expect(result).toBe('hello world'); }); it('should handle variables through resolution service', async () => { // Mock the AST with variable references in concatenation vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ['{{var1}}', '" "', '{{var2}}'], raw: '{{var1}} ++ " " ++ {{var2}}' } }) ]); vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === '{{var1}}') return 'hello'; if (value === '{{var2}}') return 'world'; if (value === '" "') return ' '; return value; }); const result = await handler.resolveConcatenation('{{var1}} ++ " " ++ {{var2}}', context); expect(result).toBe('hello world'); }); it('should preserve whitespace in string literals', async () => { // Mock AST with whitespace in string literals vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ['" hello "', '" world "'], raw: '" hello " ++ " world "' } }) ]); vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === '" hello "') return ' hello '; if (value === '" world "') return ' world '; return value; }); const result = await handler.resolveConcatenation('" hello " ++ " world "', context); expect(result).toBe(' hello world '); }); it('should handle escaped quotes in string literals', async () => { // Mock AST with escaped quotes vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ['"say \\"hello\\""', '" world"'], raw: '"say \\"hello\\"" ++ " world"' } }) ]); vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === '"say \\"hello\\""') return 'say "hello"'; if (value === '" world"') return ' world'; return value; }); const result = await handler.resolveConcatenation('"say \\"hello\\"" ++ " world"', context); expect(result).toBe('say "hello" world'); }); it('should handle mixed string literals and variables', async () => { // Mock AST with mixed string literals and variables vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ['"hello "', '{{name}}'], raw: '"hello " ++ {{name}}' } }) ]); vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === '"hello "') return 'hello '; if (value === '{{name}}') return 'world'; return value; }); const result = await handler.resolveConcatenation('"hello " ++ {{name}}', context); expect(result).toBe('hello world'); }); it('should reject empty parts', async () => { // Mock AST with invalid concatenation (empty part) vi.mocked(mockParserService.parse).mockRejectedValue(new Error('Parse error')); await expect(handler.resolveConcatenation('"hello" ++ ++ "world"', context)) .rejects .toThrow(ResolutionError); }); it('should handle resolution errors', async () => { // Mock AST with valid concatenation vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ['"hello"', '{{missing}}'], raw: '"hello" ++ {{missing}}' } }) ]); vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === '"hello"') return 'hello'; if (value === '{{missing}}') throw new ResolutionError('Variable not found', ResolutionErrorCode.VARIABLE_NOT_FOUND, { value: '{{missing}}' }); return value; }); await expect(handler.resolveConcatenation('"hello" ++ {{missing}}', context)) .rejects .toThrow(ResolutionError); }); it('should handle backtick strings', async () => { // Mock AST with backtick strings vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ['`hello`', '` world`'], raw: '`hello` ++ ` world`' } }) ]); vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === '`hello`') return 'hello'; if (value === '` world`') return ' world'; return value; }); const result = await handler.resolveConcatenation('`hello` ++ ` world`', context); expect(result).toBe('hello world'); }); it('should handle single quoted strings', async () => { // Mock AST with single quoted strings vi.mocked(mockParserService.parse).mockResolvedValue([ createDirectiveNode('text', { identifier: 'test', value: { type: 'Concatenation', parts: ["'hello'", "' world'"], raw: "'hello' ++ ' world'" } }) ]); vi.mocked(mockResolutionService.resolveInContext).mockImplementation(async (value) => { if (value === "'hello'") return 'hello'; if (value === "' world'") return ' world'; return value; }); const result = await handler.resolveConcatenation("'hello' ++ ' world'", context); expect(result).toBe('hello world'); }); }); });