UNPKG

meld

Version:

Meld: A template language for LLM prompts

733 lines (664 loc) 23.7 kB
import type { MeldNode, DirectiveNode, TextNode, CodeFenceNode, DirectiveKindString } from 'meld-spec'; import type { Location, Position } from '@core/types.js'; import type { IValidationService } from '@services/resolution/ValidationService/IValidationService.js'; import type { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import type { ICircularityService } from '@services/resolution/CircularityService/ICircularityService.js'; import type { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js'; import type { IParserService } from '@services/pipeline/ParserService/IParserService.js'; import type { IInterpreterService } from '@services/pipeline/InterpreterService/IInterpreterService.js'; import type { IPathService } from '@services/PathService/IPathService.js'; import { vi, type Mock } from 'vitest'; import { createPosition, createTestLocation as createSourceLocation, createTestDirective, createTestText, createTestCodeFence } from './nodeFactories.js'; const DEFAULT_POSITION: Position = { line: 1, column: 1 }; const DEFAULT_LOCATION: Location = { start: DEFAULT_POSITION, end: DEFAULT_POSITION, filePath: undefined }; /** * Create a location object for testing (includes filePath) */ export function createLocation( startLine: number = 1, startColumn: number = 1, endLine?: number, endColumn?: number, filePath?: string ): Location { const sourceLocation = createSourceLocation(startLine, startColumn, endLine, endColumn); return { ...sourceLocation, filePath }; } /** * Create a test directive node */ export function createTestDirective( kind: DirectiveKindString, identifier: string, value: string, location: Location = DEFAULT_LOCATION ): DirectiveNode { // For other directives, use the standard property structure return { type: 'Directive', directive: { kind, identifier, value }, location }; } /** * Create a test text node */ export function createTestText( content: string, location: Location = DEFAULT_LOCATION ): TextNode { return { type: 'Text', content, location }; } /** * Create a test code fence node */ export function createTestCodeFence( content: string, language?: string, location: Location = DEFAULT_LOCATION ): CodeFenceNode { return { type: 'CodeFence', content, language, location }; } /** * Create a test location */ export function createTestLocation( startLine: number = 1, startColumn: number = 1, endLine?: number, endColumn?: number, filePath?: string ): Location { return createLocation(startLine, startColumn, endLine, endColumn, filePath); } /** * Create a properly typed DirectiveNode for testing */ export function createDirectiveNode( kind: DirectiveKindString, properties: Record<string, any> = {}, location: Location = DEFAULT_LOCATION ): DirectiveNode { return { type: 'Directive', directive: { kind, ...properties }, location }; } /** * Create a properly typed TextNode for testing */ export function createTextNode( content: string, location: Location = DEFAULT_LOCATION ): TextNode { return { type: 'Text', content, location }; } /** * Create a properly typed CodeFenceNode for testing */ export function createCodeFenceNode( content: string, language?: string, location: Location = DEFAULT_LOCATION ): CodeFenceNode { return { type: 'CodeFence', content, language, location }; } // Create a text directive node for testing export function createTextDirective( identifier: string, value: string, location?: Location ): DirectiveNode { return createTestDirective('text', identifier, value, location); } // Create a data directive node for testing export function createDataDirective( identifier: string, value: any, location?: Location ): DirectiveNode { // Determine if this is a literal or reference source const source = 'literal'; // Return a directive node with the proper structure matching the AST return { type: 'Directive', directive: { kind: 'data', identifier, source, value }, location: location || DEFAULT_LOCATION }; } // Create a path directive node for testing export function createPathDirective( identifier: string, value: string, location?: Location ): DirectiveNode { return createTestDirective('path', identifier, value, location); } // Create a run directive node for testing export function createRunDirective( command: string, location?: Location ): DirectiveNode { return { type: 'Directive', directive: { kind: 'run', identifier: 'run', value: `[${command}]`, command }, location: location || DEFAULT_LOCATION }; } // Create an embed directive node for testing export function createEmbedDirective( path: string, section?: string, location?: Location, options?: { headingLevel?: number; underHeader?: string; fuzzy?: number; format?: string; } ): DirectiveNode { const value = section ? `[${path} # ${section}]` : `[${path}]`; return { type: 'Directive', directive: { kind: 'embed', path, value, section, ...options }, location: location || DEFAULT_LOCATION }; } // Create an import directive node for testing export function createImportDirective( imports: string, location?: Location, from?: string ): DirectiveNode { const value = from ? `[${imports}] from [${from}]` : `[${imports}]`; const path = from || imports; return { type: 'Directive', directive: { kind: 'import', identifier: 'import', value, path }, location: location || DEFAULT_LOCATION }; } // Create a define directive node for testing export function createDefineDirective( identifier: string, command: string, parameters: string[] = [], location?: Location ): DirectiveNode { const value = parameters.length > 0 ? `${identifier}(${parameters.join(', ')}) = @run [${command}]` : `${identifier} = @run [${command}]`; return { type: 'Directive', directive: { kind: 'define', identifier, value, command, parameters }, location: location || DEFAULT_LOCATION }; } // Mock service creation functions export function createMockValidationService(): IValidationService { const mockService = { validate: vi.fn(), registerValidator: vi.fn(), removeValidator: vi.fn(), hasValidator: vi.fn(), getRegisteredDirectiveKinds: vi.fn(), getAllValidators: vi.fn() }; // Set default implementations mockService.validate.mockImplementation(async () => {}); mockService.registerValidator.mockImplementation(() => {}); mockService.removeValidator.mockImplementation(() => {}); mockService.hasValidator.mockImplementation(() => false); mockService.getRegisteredDirectiveKinds.mockImplementation(() => []); mockService.getAllValidators.mockImplementation(() => []); return mockService as unknown as IValidationService; } export function createMockStateService(): IStateService { const mockService = { setTextVar: vi.fn(), getTextVar: vi.fn(), setDataVar: vi.fn(), getDataVar: vi.fn(), setPathVar: vi.fn(), getPathVar: vi.fn(), setCommand: vi.fn(), getCommand: vi.fn(), appendContent: vi.fn(), getContent: vi.fn(), createChildState: vi.fn(), getParentState: vi.fn(), isImmutable: vi.fn(), makeImmutable: vi.fn(), clone: vi.fn(), mergeStates: vi.fn(), getAllTextVars: vi.fn(), getAllDataVars: vi.fn(), getAllPathVars: vi.fn(), getAllCommands: vi.fn(), getNodes: vi.fn(), addNode: vi.fn(), getTransformedNodes: vi.fn(), transformNode: vi.fn(), isTransformationEnabled: vi.fn(), enableTransformation: vi.fn(), addImport: vi.fn(), removeImport: vi.fn(), hasImport: vi.fn(), getImports: vi.fn(), getCurrentFilePath: vi.fn(), setCurrentFilePath: vi.fn(), hasLocalChanges: vi.fn(), getLocalChanges: vi.fn(), setImmutable: vi.fn(), mergeChildState: vi.fn(), getStateId: vi.fn() }; // Set default implementations mockService.setTextVar.mockImplementation(() => {}); mockService.getTextVar.mockImplementation(() => ''); mockService.setDataVar.mockImplementation(() => {}); mockService.getDataVar.mockImplementation(() => null); mockService.setPathVar.mockImplementation(() => {}); mockService.getPathVar.mockImplementation(() => ''); mockService.setCommand.mockImplementation(() => {}); mockService.getCommand.mockImplementation(() => ''); mockService.appendContent.mockImplementation(() => {}); mockService.getContent.mockImplementation(() => ''); mockService.createChildState.mockImplementation(() => createMockStateService()); mockService.getParentState.mockImplementation(() => undefined); mockService.isImmutable.mockImplementation(() => false); mockService.makeImmutable.mockImplementation(() => {}); mockService.setImmutable.mockImplementation(() => {}); mockService.mergeChildState.mockImplementation((childState) => { // Get current state const currentTextVars = mockService.getAllTextVars(); const currentDataVars = mockService.getAllDataVars(); const currentPathVars = mockService.getAllPathVars(); const currentCommands = mockService.getAllCommands(); const currentNodes = mockService.getNodes(); const currentTransformedNodes = mockService.getTransformedNodes(); const currentImports = mockService.getImports(); // Get child state const childTextVars = childState.getAllTextVars(); const childDataVars = childState.getAllDataVars(); const childPathVars = childState.getAllPathVars(); const childCommands = childState.getAllCommands(); const childNodes = childState.getNodes(); const childTransformedNodes = childState.getTransformedNodes(); const childImports = childState.getImports(); // Merge variables const mergedTextVars = new Map([...currentTextVars, ...childTextVars]); const mergedDataVars = new Map([...currentDataVars, ...childDataVars]); const mergedPathVars = new Map([...currentPathVars, ...childPathVars]); const mergedCommands = new Map([...currentCommands, ...childCommands]); const mergedNodes = [...currentNodes, ...childNodes]; const mergedImports = new Set([...currentImports, ...childImports]); // Handle transformed nodes let mergedTransformedNodes; if (mockService.isTransformationEnabled()) { if (childTransformedNodes && childTransformedNodes.length > 0) { mergedTransformedNodes = currentTransformedNodes ? [...currentTransformedNodes, ...childTransformedNodes] : [...childTransformedNodes]; } else { mergedTransformedNodes = currentTransformedNodes; } } // Update mock implementations with merged state mockService.getAllTextVars.mockImplementation(() => mergedTextVars); mockService.getAllDataVars.mockImplementation(() => mergedDataVars); mockService.getAllPathVars.mockImplementation(() => mergedPathVars); mockService.getAllCommands.mockImplementation(() => mergedCommands); mockService.getNodes.mockImplementation(() => mergedNodes); if (mergedTransformedNodes) { mockService.getTransformedNodes.mockImplementation(() => mergedTransformedNodes); } mockService.getImports.mockImplementation(() => mergedImports); // Update individual getters mockService.getTextVar.mockImplementation((name) => mergedTextVars.get(name)); mockService.getDataVar.mockImplementation((name) => mergedDataVars.get(name)); mockService.getPathVar.mockImplementation((name) => mergedPathVars.get(name)); mockService.getCommand.mockImplementation((name) => mergedCommands.get(name)); mockService.hasImport.mockImplementation((path) => mergedImports.has(path)); // If tracking service is available, add merge relationship if (mockService.trackingService && childState.getStateId()) { mockService.trackingService.addRelationship( mockService.getStateId()!, childState.getStateId()!, 'merge-source' ); } }); mockService.clone.mockImplementation(() => { const newMock = createMockStateService(); // Copy all state newMock.getTextVar.mockImplementation(mockService.getTextVar); newMock.getDataVar.mockImplementation(mockService.getDataVar); newMock.getPathVar.mockImplementation(mockService.getPathVar); newMock.getCommand.mockImplementation(mockService.getCommand); newMock.getAllTextVars.mockImplementation(mockService.getAllTextVars); newMock.getAllDataVars.mockImplementation(mockService.getAllDataVars); newMock.getAllPathVars.mockImplementation(mockService.getAllPathVars); newMock.getAllCommands.mockImplementation(mockService.getAllCommands); newMock.getNodes.mockImplementation(mockService.getNodes); newMock.getTransformedNodes.mockImplementation(mockService.getTransformedNodes); newMock.isTransformationEnabled.mockImplementation(mockService.isTransformationEnabled); newMock.getCurrentFilePath.mockImplementation(mockService.getCurrentFilePath); newMock.hasLocalChanges.mockImplementation(mockService.hasLocalChanges); newMock.getLocalChanges.mockImplementation(mockService.getLocalChanges); newMock.isImmutable.mockImplementation(mockService.isImmutable); newMock.getImports.mockImplementation(mockService.getImports); newMock.hasImport.mockImplementation(mockService.hasImport); newMock.getStateId.mockImplementation(mockService.getStateId); // Copy service references if (mockService.eventService) { newMock.setEventService(mockService.eventService); } if (mockService.trackingService) { newMock.setTrackingService(mockService.trackingService); } return newMock; }); // Restore other mock implementations mockService.getAllTextVars.mockImplementation(() => new Map()); mockService.getAllDataVars.mockImplementation(() => new Map()); mockService.getAllPathVars.mockImplementation(() => new Map()); mockService.getAllCommands.mockImplementation(() => new Map()); mockService.getNodes.mockImplementation(() => []); mockService.addNode.mockImplementation(() => {}); mockService.getTransformedNodes.mockImplementation(() => []); // Enhanced transformNode implementation mockService.transformNode.mockImplementation((original, transformed) => { // Check if transformation is enabled if (!mockService.isTransformationEnabled()) { return; } // Get current nodes const nodes = mockService.getNodes(); const transformedNodes = mockService.getTransformedNodes() || [...nodes]; // Try to find the node by reference first let index = transformedNodes.findIndex(node => node === original); // If not found by reference, try matching by properties if (index === -1) { index = transformedNodes.findIndex(node => node.type === original.type && node.content === original.content && node.location.start.line === original.location.start.line && node.location.start.column === original.location.start.column && node.location.end.line === original.location.end.line && node.location.end.column === original.location.end.column ); } if (index !== -1) { transformedNodes[index] = transformed; mockService.getTransformedNodes.mockImplementation(() => transformedNodes); } else { // If not found in transformed nodes, check original nodes const originalIndex = nodes.findIndex(node => node === original); if (originalIndex === -1) { throw new Error('Cannot transform node: original node not found'); } transformedNodes.push(transformed); mockService.getTransformedNodes.mockImplementation(() => transformedNodes); } }); mockService.isTransformationEnabled.mockImplementation(() => false); mockService.enableTransformation.mockImplementation(() => {}); mockService.addImport.mockImplementation(() => {}); mockService.removeImport.mockImplementation(() => {}); mockService.hasImport.mockImplementation(() => false); mockService.getImports.mockImplementation(() => new Set()); mockService.getCurrentFilePath.mockImplementation(() => null); mockService.setCurrentFilePath.mockImplementation(() => {}); mockService.hasLocalChanges.mockImplementation(() => false); mockService.getLocalChanges.mockImplementation(() => []); return mockService as unknown as IStateService; } export function createMockResolutionService(): IResolutionService { const mockService = { resolveInContext: vi.fn(), resolveContent: vi.fn(), resolvePath: vi.fn(), resolveCommand: vi.fn(), resolveText: vi.fn(), resolveData: vi.fn(), validateResolution: vi.fn(), extractSection: vi.fn() }; // Set default implementations mockService.resolveInContext.mockImplementation(async (value: string, context: any) => { // Validate string literals if (value.startsWith("'") || value.startsWith('"') || value.startsWith('`')) { const quote = value[0]; if (value[value.length - 1] !== quote) { throw new Error('Unclosed string literal'); } // Check for unescaped quotes const content = value.slice(1, -1); const unescapedQuotes = new RegExp(`(?<!\\\\)${quote}`, 'g'); if (unescapedQuotes.test(content)) { throw new Error('Invalid string literal: unescaped quotes'); } // Return unescaped content return content.replace(new RegExp(`\\\\${quote}`, 'g'), quote); } // Handle variable references const varPattern = /\${([^}]+)}/g; return value.replace(varPattern, (match, varPath) => { const parts = varPath.split('.'); const baseVar = parts[0]; // Check for environment variables if (baseVar.startsWith('ENV_')) { return process.env[baseVar] || ''; } // Try text variables first let varValue = context.state.getTextVar(baseVar); // Then try data variables if allowed if (varValue === undefined && context.allowedVariableTypes?.data) { varValue = context.state.getDataVar(baseVar); if (varValue && parts.length > 1) { // Handle nested data access for (let i = 1; i < parts.length; i++) { varValue = varValue[parts[i]]; } } } if (varValue === undefined) { throw new Error(`Undefined variable: ${baseVar}`); } return String(varValue); }); }); mockService.resolveContent.mockImplementation(async (nodes) => { return nodes.map(n => n.type === 'Text' ? n.content : '').join(''); }); mockService.resolvePath.mockImplementation(async (path) => path); mockService.resolveCommand.mockImplementation(async (cmd) => cmd); mockService.resolveText.mockImplementation(async (text) => text); mockService.resolveData.mockImplementation(async (ref) => ref); mockService.validateResolution.mockImplementation(async () => {}); mockService.extractSection.mockImplementation(async () => ''); return mockService as unknown as IResolutionService; } export function createMockFileSystemService(): IFileSystemService { const mockService = { readFile: vi.fn(), writeFile: vi.fn(), exists: vi.fn(), stat: vi.fn(), isFile: vi.fn(), readDir: vi.fn(), ensureDir: vi.fn(), isDirectory: vi.fn(), join: vi.fn(), resolve: vi.fn(), dirname: vi.fn(), basename: vi.fn(), normalize: vi.fn(), executeCommand: vi.fn(), getCwd: vi.fn(), enableTestMode: vi.fn(), disableTestMode: vi.fn(), isTestMode: vi.fn(), mockFile: vi.fn(), mockDir: vi.fn(), clearMocks: vi.fn() }; // Set default implementations mockService.readFile.mockImplementation(async () => ''); mockService.writeFile.mockImplementation(async () => {}); mockService.exists.mockImplementation(async () => true); mockService.stat.mockImplementation(async () => ({})); mockService.isFile.mockImplementation(async () => true); mockService.readDir.mockImplementation(async () => []); mockService.ensureDir.mockImplementation(async () => {}); mockService.isDirectory.mockImplementation(async () => false); mockService.join.mockImplementation((...paths) => paths.join('/')); mockService.resolve.mockImplementation((path) => path); mockService.dirname.mockImplementation((path) => path.split('/').slice(0, -1).join('/')); mockService.basename.mockImplementation((path) => path.split('/').pop() || ''); mockService.normalize.mockImplementation((path) => path); mockService.executeCommand.mockImplementation(async () => ({ stdout: '', stderr: '' })); mockService.getCwd.mockImplementation(() => '/project'); mockService.enableTestMode.mockImplementation(() => {}); mockService.disableTestMode.mockImplementation(() => {}); mockService.isTestMode.mockImplementation(() => true); mockService.mockFile.mockImplementation(() => {}); mockService.mockDir.mockImplementation(() => {}); mockService.clearMocks.mockImplementation(() => {}); // Bind all functions to the mock service Object.keys(mockService).forEach(key => { const fn = mockService[key]; mockService[key] = fn.bind(mockService); }); return mockService as unknown as IFileSystemService; } export function createMockCircularityService(): ICircularityService { const mockService = { beginImport: vi.fn(), endImport: vi.fn(), isImporting: vi.fn(), getImportChain: vi.fn() }; // Set default implementations mockService.beginImport.mockImplementation(async () => {}); mockService.endImport.mockImplementation(async () => {}); mockService.isImporting.mockImplementation(() => false); mockService.getImportChain.mockImplementation(() => []); return mockService as unknown as ICircularityService; } export function createMockParserService(): IParserService { const mockService = { parse: vi.fn(), parseWithLocations: vi.fn() }; // Set default implementations mockService.parse.mockImplementation(async () => []); mockService.parseWithLocations.mockImplementation(async () => []); return mockService as unknown as IParserService; } export function createMockInterpreterService(): IInterpreterService { const mockService = { interpret: vi.fn(), interpretWithContext: vi.fn() }; // Set default implementations mockService.interpret.mockImplementation(async () => {}); mockService.interpretWithContext.mockImplementation(async () => {}); return mockService as unknown as IInterpreterService; } export function createMockPathService(): IPathService { const mockService = { resolvePath: vi.fn(), normalizePath: vi.fn(), isAbsolute: vi.fn(), join: vi.fn(), dirname: vi.fn(), basename: vi.fn(), extname: vi.fn(), relative: vi.fn() }; // Set default implementations mockService.resolvePath.mockImplementation(() => ''); mockService.normalizePath.mockImplementation(() => ''); mockService.isAbsolute.mockImplementation(() => false); mockService.join.mockImplementation(() => ''); mockService.dirname.mockImplementation(() => ''); mockService.basename.mockImplementation(() => ''); mockService.extname.mockImplementation(() => ''); mockService.relative.mockImplementation(() => ''); return mockService as unknown as IPathService; }