UNPKG

meld

Version:

Meld: A template language for LLM prompts

316 lines (287 loc) 10.5 kB
import { Volume } from 'memfs'; import * as path from 'path'; import type { Stats } from 'fs'; import { filesystemLogger as logger } from '@core/utils/logger.js'; import { IFileSystem } from '@services/fs/FileSystemService/IFileSystem.js'; import { MockCommandExecutor, CommandResponse, createCommonCommandMappings } from './MockCommandExecutor.js'; /** * File system implementation that combines memfs with mock command execution * Designed for comprehensive test coverage with full control over file operations and command execution */ export class CommandMockableFileSystem implements IFileSystem { private vol: Volume; private root: string = '/'; public commandExecutor: MockCommandExecutor; constructor() { logger.debug('Initializing CommandMockableFileSystem'); try { this.vol = new Volume(); // Initialize root directory this.vol.mkdirSync(this.root, { recursive: true }); // Initialize command executor with common patterns this.commandExecutor = new MockCommandExecutor(createCommonCommandMappings()); logger.debug('CommandMockableFileSystem initialized'); } catch (error) { logger.error('Error initializing CommandMockableFileSystem', { error }); throw new Error(`Error initializing CommandMockableFileSystem: ${error.message}`); } } /** * Initialize or reset the filesystem */ initialize(): void { logger.debug('Resetting filesystem'); try { this.vol.reset(); // Re-initialize root this.vol.mkdirSync(this.root, { recursive: true }); // Reset command executor this.commandExecutor.reset(); this.commandExecutor.setMapping(createCommonCommandMappings()); logger.debug('Filesystem reset complete'); } catch (error) { logger.error('Error initializing filesystem', { error }); throw new Error(`Error initializing filesystem: ${error.message}`); } } /** * Clean up any resources */ async cleanup(): Promise<void> { logger.debug('Cleaning up filesystem'); try { // Reset the volume first to clear everything this.vol.reset(); // Re-initialize root this.vol.mkdirSync(this.root, { recursive: true }); // Reset command executor this.commandExecutor.reset(); logger.debug('Filesystem cleanup complete'); } catch (error) { logger.error('Error during cleanup', { error }); throw new Error(`Error during cleanup: ${error.message}`); } } /** * Read a file's contents */ async readFile(filePath: string): Promise<string> { logger.debug('Reading file', { filePath }); try { // Check if file exists if (!this.vol.existsSync(filePath)) { logger.error('File not found', { filePath }); throw new Error(`ENOENT: no such file or directory: ${filePath}`); } // Get stats and check if directory const stats = this.vol.statSync(filePath); if (stats.isDirectory()) { logger.error('Cannot read directory as file', { filePath }); throw new Error(`EISDIR: Cannot read directory as file: ${filePath}`); } // Read the file const content = this.vol.readFileSync(filePath, 'utf-8'); logger.debug('File read successfully', { filePath, contentLength: content.length }); return content; } catch (error) { // If error is already formatted, just rethrow if (error.message.startsWith('EISDIR:') || error.message.startsWith('ENOENT:')) { throw error; } // Otherwise wrap in a more descriptive error logger.error('Error reading file', { filePath, error }); throw new Error(`Error reading file '${filePath}': ${error.message}`); } } /** * Write a file */ async writeFile(filePath: string, content: string): Promise<void> { logger.debug('Writing file', { filePath }); const dirPath = path.dirname(filePath); try { // Create parent directories if they don't exist if (!this.vol.existsSync(dirPath)) { this.vol.mkdirSync(dirPath, { recursive: true }); } this.vol.writeFileSync(filePath, content, 'utf-8'); logger.debug('File written successfully', { filePath }); } catch (error) { logger.error('Error writing file', { filePath, error }); throw new Error(`Error writing file ${filePath}: ${error.message}`); } } /** * Check if a file or directory exists */ async exists(filePath: string): Promise<boolean> { logger.debug('Checking if path exists', { filePath }); try { const exists = this.vol.existsSync(filePath); logger.debug('Path existence check result', { filePath, exists }); return exists; } catch (error) { logger.error('Error checking path existence', { filePath, error }); return false; } } /** * Get stats for a file or directory */ async stat(filePath: string): Promise<Stats> { logger.debug('Getting stats', { filePath }); try { const stats = this.vol.statSync(filePath) as Stats; logger.debug('Got stats', { filePath, isDirectory: stats.isDirectory(), isFile: stats.isFile(), size: stats.size }); return stats; } catch (error) { logger.error('Error getting stats', { filePath, error }); throw new Error(`Error getting stats for '${filePath}': ${error.message}`); } } /** * Read directory contents */ async readDir(dirPath: string): Promise<string[]> { logger.debug('Reading directory', { dirPath }); try { // Check if path exists if (!this.vol.existsSync(dirPath)) { logger.error('Directory not found', { dirPath }); throw new Error(`ENOENT: no such directory: ${dirPath}`); } // Check if it's a directory const stats = this.vol.statSync(dirPath); if (!stats.isDirectory()) { logger.error('Path is not a directory', { dirPath }); throw new Error(`ENOTDIR: not a directory: ${dirPath}`); } // Read directory entries const entries = this.vol.readdirSync(dirPath); // Ensure we have a valid array and convert entries to strings if (!Array.isArray(entries)) { logger.debug('Directory read did not return array, returning empty array', { dirPath }); return []; } // Convert any Dirent objects or other types to strings const stringEntries = entries.map(entry => entry.toString()); logger.debug('Directory read successful', { dirPath, entryCount: stringEntries.length }); return stringEntries; } catch (error) { if (error.message.startsWith('ENOENT:') || error.message.startsWith('ENOTDIR:')) { throw error; } logger.error('Error reading directory', { dirPath, error }); throw new Error(`Error reading directory '${dirPath}': ${error.message}`); } } /** * Create a directory */ async mkdir(dirPath: string): Promise<void> { logger.debug('Creating directory', { dirPath }); try { // Check if path exists if (this.vol.existsSync(dirPath)) { const stats = this.vol.statSync(dirPath); if (stats.isDirectory()) { logger.debug('Directory already exists', { dirPath }); return; } logger.error('Path exists but is not a directory', { dirPath }); throw new Error(`ENOTDIR: path exists but is not a directory: ${dirPath}`); } // Create directory with recursive option this.vol.mkdirSync(dirPath, { recursive: true }); logger.debug('Directory created successfully', { dirPath }); } catch (error) { // If error is already formatted, just rethrow if (error.message.startsWith('ENOTDIR:')) { throw error; } // Otherwise wrap in a more descriptive error logger.error('Error creating directory', { dirPath, error }); throw new Error(`Error creating directory '${dirPath}': ${error.message}`); } } /** * Check if path is a directory */ async isDirectory(filePath: string): Promise<boolean> { logger.debug('Checking if path is directory', { filePath }); try { if (!this.vol.existsSync(filePath)) { return false; } const stats = this.vol.statSync(filePath); const isDir = stats.isDirectory(); logger.debug('Directory check result', { filePath, isDir }); return isDir; } catch (error) { logger.error('Error checking if path is directory', { filePath, error }); return false; } } /** * Check if path is a file */ async isFile(filePath: string): Promise<boolean> { logger.debug('Checking if path is file', { filePath }); try { if (!this.vol.existsSync(filePath)) { return false; } const stats = this.vol.statSync(filePath); const isFile = stats.isFile(); logger.debug('File check result', { filePath, isFile }); return isFile; } catch (error) { logger.error('Error checking if path is file', { filePath, error }); return false; } } /** * Watch a file or directory for changes (simplified implementation for tests) */ async *watch( path: string, options?: { recursive?: boolean } ): AsyncIterableIterator<{ filename: string; eventType: string }> { logger.debug('Watch not fully implemented in CommandMockableFileSystem'); // This is a simplified implementation that doesn't actually watch // In real tests, you would trigger events manually return { filename: path, eventType: 'change' }; } /** * Get current working directory */ getCwd(): string { return this.root; } /** * Execute a command using the mock command executor */ async executeCommand(command: string, options?: { cwd?: string }): Promise<CommandResponse> { logger.debug('Executing command', { command, cwd: options?.cwd }); try { const result = await this.commandExecutor.executeCommand(command, options); logger.debug('Command executed successfully', { command, stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode }); return result; } catch (error) { logger.error('Error executing command', { command, error }); throw new Error(`Error executing command: ${command}: ${error.message}`); } } }