UNPKG

meld

Version:

Meld: A template language for LLM prompts

380 lines (315 loc) 13.9 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { TextDirectiveHandler } from './TextDirectiveHandler.js'; import { createMockStateService, createMockValidationService, createMockResolutionService } from '@tests/utils/testFactories.js'; import { DirectiveError } from '@services/pipeline/DirectiveService/errors/DirectiveError.js'; import type { DirectiveNode } from 'meld-spec'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import { StringLiteralHandler } from '@services/resolution/ResolutionService/resolvers/StringLiteralHandler.js'; import { StringConcatenationHandler } from '@services/resolution/ResolutionService/resolvers/StringConcatenationHandler.js'; // Import the centralized syntax examples and helpers import { textDirectiveExamples } from '@core/syntax/index.js'; import { ErrorSeverity } from '@core/errors'; /** * Helper function to create real AST nodes using meld-ast */ const createNodeFromExample = async (code: string): Promise<DirectiveNode> => { try { const { parse } = await import('meld-ast'); const result = await parse(code, { trackLocations: true, validateNodes: true, structuredPaths: true }); return result.ast[0] as DirectiveNode; } catch (error) { console.error('Error parsing with meld-ast:', error); throw error; } }; describe('TextDirectiveHandler', () => { let handler: TextDirectiveHandler; let stateService: ReturnType<typeof createMockStateService>; let validationService: ReturnType<typeof createMockValidationService>; let resolutionService: ReturnType<typeof createMockResolutionService>; let clonedState: IStateService; // Create real instances of the literal and concatenation handlers for testing let realStringLiteralHandler: StringLiteralHandler; let realStringConcatenationHandler: StringConcatenationHandler; beforeEach(() => { clonedState = { setTextVar: vi.fn(), getTextVar: vi.fn(), getDataVar: vi.fn(), clone: vi.fn(), } as unknown as IStateService; stateService = { setTextVar: vi.fn(), getTextVar: vi.fn(), getDataVar: vi.fn(), clone: vi.fn().mockReturnValue(clonedState) } as unknown as IStateService; validationService = createMockValidationService(); resolutionService = createMockResolutionService(); // Create real handlers to match actual implementation realStringLiteralHandler = new StringLiteralHandler(); realStringConcatenationHandler = new StringConcatenationHandler(resolutionService); // Set up better mocking for variable resolution resolutionService.resolveInContext.mockImplementation(async (value: string, context: any) => { // Use real string literal handler for string literals if (realStringLiteralHandler.isStringLiteral(value)) { return realStringLiteralHandler.parseLiteral(value); } // Handle common test case values - this simulates what the real ResolutionService would do if (value.includes('{{name}}')) { return value.replace(/\{\{name\}\}/g, 'World'); } if (value.includes('{{user.name}}')) { return value.replace(/\{\{user\.name\}\}/g, 'Alice'); } if (value.includes('{{ENV_HOME}}')) { return value.replace(/\{\{ENV_HOME\}\}/g, '/home/user'); } if (value.includes('{{missing}}')) { throw new Error('Variable not found: missing'); } // Special case for pass-through directives test if (value === '"@run echo \\"test\\""') { return '@run echo "test"'; } // For string concatenation tests if (value === '"Hello" ++ " " ++ "World"') { return 'Hello World'; } if (value === '"Hello " ++ "{{name}}"') { return 'Hello World'; } if (value === '"Prefix: " ++ "Header" ++ "Footer"') { return 'Prefix: HeaderFooter'; } if (value === '"double" ++ \'single\' ++ `backtick`') { return 'doublesinglebacktick'; } return value; }); // Mock validation service to fail for invalid nodes validationService.validate.mockImplementation((node: any) => { if (node.directive?.value === "'unclosed string") { throw new Error('Invalid string literal: unclosed string'); } if (node.directive?.value === '"no"++"spaces"') { throw new Error('Invalid concatenation syntax'); } return Promise.resolve(); }); handler = new TextDirectiveHandler(validationService, stateService, resolutionService); }); describe('execute', () => { it('should handle a simple text assignment with string literal', async () => { // Arrange const example = textDirectiveExamples.atomic.simpleString; const node = await createNodeFromExample(example.code); const context = { state: stateService, currentFilePath: 'test.meld' }; const result = await handler.execute(node, context); // The example uses 'greeting' as the identifier and "Hello" as the value expect(clonedState.setTextVar).toHaveBeenCalledWith('greeting', 'Hello'); }); it('should handle text assignment with escaped characters', async () => { // Arrange const example = textDirectiveExamples.atomic.escapedCharacters; const node = await createNodeFromExample(example.code); // Special case - direct mock to handle expected behavior resolutionService.resolveInContext.mockImplementation(async () => { return 'Line 1\nLine 2\t"Quoted"'; }); const context = { state: stateService, currentFilePath: 'test.meld' }; const result = await handler.execute(node, context); expect(clonedState.setTextVar).toHaveBeenCalledWith('escaped', 'Line 1\nLine 2\t"Quoted"'); }); it('should handle a template literal in text directive', async () => { // Arrange const example = textDirectiveExamples.atomic.templateLiteral; const node = await createNodeFromExample(example.code); const context = { state: stateService, currentFilePath: 'test.meld' }; const result = await handler.execute(node, context); expect(clonedState.setTextVar).toHaveBeenCalledWith('message', 'Template content'); }); it('should handle object property interpolation in text value', async () => { // Arrange const example = textDirectiveExamples.combinations.objectInterpolation; // For this test, we need a custom implementation const mockResolveInContext = resolutionService.resolveInContext; resolutionService.resolveInContext = vi.fn().mockImplementation(() => { return 'Hello, Alice! Your ID is 123.'; }); const node = await createNodeFromExample(example.code.split('\n')[1]); // Get the second line with greeting directive const context = { state: stateService, currentFilePath: 'test.meld' }; const result = await handler.execute(node, context); expect(clonedState.setTextVar).toHaveBeenCalledWith('greeting', 'Hello, Alice! Your ID is 123.'); // Restore the original mock resolutionService.resolveInContext = mockResolveInContext; }); it('should handle path referencing in text values', async () => { // Arrange const example = textDirectiveExamples.combinations.pathReferencing; // For this test, we need a custom implementation const mockResolveInContext = resolutionService.resolveInContext; resolutionService.resolveInContext = vi.fn().mockImplementation(() => { return 'Docs are at $PROJECTPATH/docs'; }); const node = await createNodeFromExample(example.code.split('\n')[5]); // Get the docsText line const context = { state: stateService, currentFilePath: 'test.meld' }; const result = await handler.execute(node, context); expect(clonedState.setTextVar).toHaveBeenCalledWith('configText', 'Docs are at $PROJECTPATH/docs'); // Restore the original mock resolutionService.resolveInContext = mockResolveInContext; }); it('should return error if text interpolation contains undefined variables', async () => { // Arrange const example = textDirectiveExamples.invalid.undefinedVariable; // For error testing, we need to create a custom implementation // that throws an error for this specific test const mockResolveInContext = resolutionService.resolveInContext; resolutionService.resolveInContext = vi.fn().mockImplementation(() => { throw new Error('Variable not found: undefined_var'); }); const node = await createNodeFromExample(example.code); const context = { state: stateService, currentFilePath: 'test.meld' }; await expect(handler.execute(node, context)) .rejects .toThrow(DirectiveError); // Restore the original mock resolutionService.resolveInContext = mockResolveInContext; }); it('should handle basic variable interpolation', async () => { // Arrange const example = textDirectiveExamples.combinations.basicInterpolation; // For this test, we need a custom implementation const mockResolveInContext = resolutionService.resolveInContext; resolutionService.resolveInContext = vi.fn().mockImplementation(() => { return 'Hello, World!'; }); const node = await createNodeFromExample(example.code.split('\n')[2]); // Get the third line with the message directive const context = { state: stateService, currentFilePath: 'test.meld' }; const result = await handler.execute(node, context); expect(clonedState.setTextVar).toHaveBeenCalledWith('message', 'Hello, World!'); // Restore the original mock resolutionService.resolveInContext = mockResolveInContext; }); it('should register the node as a text directive in the registry', async () => { // Arrange const example = textDirectiveExamples.atomic.simpleString; const node = await createNodeFromExample(example.code); const context = { state: stateService, currentFilePath: 'test.meld' }; const result = await handler.execute(node, context); // The example uses 'greeting' as the identifier and "Hello" as the value expect(clonedState.setTextVar).toHaveBeenCalledWith('greeting', 'Hello'); }); it('should report error for unclosed string', async () => { // For invalid test cases, we'll need to manually create nodes // since meld-ast would throw on these during parsing const invalidExample = textDirectiveExamples.invalid.unclosedString; // Create a mock node directly instead of parsing invalid syntax const node = { type: 'Directive', directive: { kind: 'text', identifier: 'greeting', value: '"unclosed string' }, location: { start: { line: 1, column: 1 }, end: { line: 1, column: 30 } } }; // Ensure our mock validation service rejects this validationService.validate.mockRejectedValueOnce(new Error('Invalid string literal: unclosed string')); await expect(handler.execute(node, { state: stateService, currentFilePath: 'test.meld' })) .rejects .toThrow(DirectiveError); }); }); /** * This section demonstrates how to use the centralized syntax system * once the import issues are fixed. * * NOTE: This section is commented out until the centralized system imports * are working properly. */ /* describe('centralized syntax examples (future implementation)', () => { it('should handle atomic examples correctly', async () => { // Using the centralized atomic examples const example = getExample('text', 'atomic', 'simpleString'); const node = await createNodeFromExample(example.code); const context = { state: stateService, currentFilePath: 'test.meld' }; await handler.execute(node, context); expect(clonedState.setTextVar).toHaveBeenCalledWith('greeting', 'Hello'); }); it('should reject invalid examples', async () => { // Using the centralized invalid examples // Note: For invalid syntax, we need to manually create nodes since parsing would fail const invalidExample = getInvalidExample('text', 'unclosedString'); // Create a node that represents what the parser would have created // if it didn't throw on invalid syntax const node: DirectiveNode = { type: 'Directive', directive: { kind: 'text', identifier: 'invalid', value: invalidExample.code.split('=')[1]?.trim() || '' }, location: { start: { line: 1, column: 1 }, end: { line: 1, column: invalidExample.code.length } } }; const context = { state: stateService, currentFilePath: 'test.meld' }; // Make the validation service reject this as expected by the invalid example validationService.validate.mockRejectedValueOnce( new Error(invalidExample.expectedError.message) ); await expect(handler.execute(node, context)) .rejects .toThrow(DirectiveError); }); it('should test multiple examples in bulk', async () => { // This is a demonstration of using testParserWithValidExamples // to test multiple examples at once testParserWithValidExamples(handler, 'text', 'atomic'); }); }); */ });