UNPKG

meld

Version:

Meld: A template language for LLM prompts

215 lines (189 loc) 6.93 kB
/** * Integrated helper for CLI testing * * This utility combines all the individual test utilities into a single helper * for comprehensive CLI testing. */ import { TestContext } from '@tests/utils/TestContext.js'; import { MemfsTestFileSystemAdapter } from '@tests/utils/MemfsTestFileSystemAdapter.js'; import { FileSystemService } from '@services/fs/FileSystemService/FileSystemService.js'; import { PathService } from '@services/fs/PathService/PathService.js'; import { PathOperationsService } from '@services/fs/FileSystemService/PathOperationsService.js'; import { mockProcessExit } from './mockProcessExit.js'; import { mockConsole } from './mockConsole.js'; import { ReturnType } from 'vitest'; import * as path from 'path'; /** * Options for setting up a CLI test */ interface CliTestOptions { /** Files to create in the mock file system */ files?: Record<string, string>; /** Environment variables to set */ env?: Record<string, string>; /** Whether to mock process.exit */ mockProcessExit?: boolean; /** Whether to mock console output */ mockConsole?: boolean; /** Whether to create default test file */ createDefaultTestFile?: boolean; /** Project root path (defaults to /project) */ projectRoot?: string; } /** * Result of setupCliTest call */ interface CliTestResult { /** The TestContext instance */ context: TestContext; /** The filesystem adapter for the test */ fsAdapter: MemfsTestFileSystemAdapter; /** The FileSystemService instance */ fileSystemService: FileSystemService; /** The PathService instance */ pathService: PathService; /** Mock function for process.exit */ exitMock?: ReturnType<typeof mockProcessExit>['mockExit']; /** Mock functions for console methods */ consoleMocks?: ReturnType<typeof mockConsole>['mocks']; /** Function to clean up all mocks */ cleanup: () => void; } /** * Set up a CLI test environment with all necessary mocks * @param options - Options for setting up the test * @returns Object containing mock functions and a cleanup function */ export function setupCliTest(options: CliTestOptions = {}): CliTestResult { const context = new TestContext(); const fsAdapter = new MemfsTestFileSystemAdapter(context.fs); const pathOps = new PathOperationsService(); const fileSystemService = new FileSystemService(pathOps, fsAdapter); const pathService = new PathService(); // Initialize services pathService.initialize(fileSystemService); pathService.enableTestMode(); // Add spy for getFileSystem method to track calls vi.spyOn(fileSystemService, 'getFileSystem'); const projectRoot = options.projectRoot || '/project'; // Create project directory fsAdapter.mkdirSync(projectRoot, { recursive: true }); // Set up mock file system const files = options.files || {}; // Create default test files if needed if (options.createDefaultTestFile || Object.keys(files).length === 0) { // Create the default test file at /project/test.meld if not already specified const defaultTestPath = `${projectRoot}/test.meld`; if (!files[defaultTestPath]) { files[defaultTestPath] = '# Default test file'; } // Create the test file at ./test.meld for $./test.meld path format if (!files['./test.meld']) { files['./test.meld'] = '# Default test file'; } } // Create all files in the mock filesystem Object.entries(files).forEach(([filePath, content]) => { try { // Fixed path format for CLI tests let testPath = filePath; // Add special path prefix if needed for absolute paths starting with /project/ if (filePath.startsWith('/project/') && !filePath.startsWith('$')) { testPath = '$.' + filePath.substring('/project'.length); // Handle special case for just /project if (testPath === '$./') { testPath = '$.'; } } console.log(`Setting up test file: ${testPath} (original: ${filePath})`); // Resolve special paths for memfs handling const resolvedPath = fsAdapter.resolveSpecialPaths(filePath); // Ensure parent directory exists const dirPath = path.dirname(resolvedPath); if (dirPath && dirPath !== '.') { console.log(`Creating parent directory: ${dirPath}`); fsAdapter.mkdirSync(dirPath, { recursive: true }); } // Write the file fsAdapter.writeFileSync(resolvedPath, content); console.log(`Created test file: ${resolvedPath} (from: ${filePath}, test path: ${testPath})`); } catch (error) { console.warn(`Failed to write file: ${filePath}`, error); } }); // Set up environment variables if (options.env) { const originalEnv = { ...process.env }; Object.entries(options.env).forEach(([key, value]) => { process.env[key] = value; }); } // Set up process.exit mock if requested const exitMock = options.mockProcessExit !== false ? mockProcessExit() : null; // Set up console mocks if requested const consoleMocks = options.mockConsole !== false ? mockConsole() : null; return { context, fsAdapter, fileSystemService, pathService, exitMock: exitMock?.mockExit || vi.fn(), consoleMocks: consoleMocks?.mocks, cleanup: () => { // Restore mocks exitMock?.restore(); consoleMocks?.restore(); // Additional cleanup for Vitest vi.clearAllMocks(); // Restore environment variables if (options.env) { Object.keys(options.env).forEach((key) => { delete process.env[key]; }); } // Cleanup the context context.cleanup(); } }; } /** * Example usage: * * ```typescript * describe('CLI', () => { * it('should process template with environment variables', async () => { * const { exitMock, consoleMock, vol, cleanup } = setupCliTest({ * files: { * '/template.meld': '@text greeting = "Hello #{env.USER}"' * }, * env: { * 'USER': 'TestUser' * } * }); * * try { * await cli.run(['template.meld', '--output', 'result.txt']); * expect(exitMock).not.toHaveBeenCalled(); * expect(vol.existsSync('/result.txt')).toBe(true); * expect(vol.readFileSync('/result.txt', 'utf8')).toBe('Hello TestUser'); * } finally { * cleanup(); * } * }); * * it('should handle errors in strict mode', async () => { * const { exitMock, consoleMock, cleanup } = setupCliTest(); * * try { * await cli.run(['--strict', '--eval', '@text greeting = "Hello #{undefined}"']); * expect(exitMock).toHaveBeenCalledWith(1); * expect(consoleMock.error).toHaveBeenCalledWith( * expect.stringContaining('undefined variable') * ); * } finally { * cleanup(); * } * }); * }); * ``` */