meld
Version:
Meld: A template language for LLM prompts
316 lines (287 loc) • 10.5 kB
text/typescript
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}`);
}
}
}