/** * Test Command * Generates PHPUnit test files for PHP classes */ import chalk from 'chalk'; import ora from 'ora'; import { writeFile, mkdir } from 'fs/promises'; import { resolve, join } from 'path'; import { scanPhpFiles, extractClassInfo } from '../scanner.js'; import { OllamaClient } from '../ollama.js'; /** * Options for the test command */ export interface TestOptions { output: string; model: string; } /** * Generated test file info */ interface GeneratedTest { class: string; testFile: string; methods: number; } /** * Execute the test command * @param targetPath - Path to PHP file(s) to generate tests for * @param options - Command options */ export async function testCommand( targetPath: string, options: TestOptions ): Promise { const spinner = ora('Initializing...').start(); try { // Initialize Ollama client const ollama = new OllamaClient({ model: options.model }); // Check Ollama connection spinner.text = 'Checking Ollama connection...'; const isConnected = await ollama.checkConnection(); if (!isConnected) { spinner.fail(chalk.red('Cannot connect to Ollama')); console.log(chalk.yellow('\nMake sure Ollama is running:')); console.log(chalk.gray(' 1. Install Ollama: https://ollama.ai')); console.log(chalk.gray(' 2. Start server: ollama serve')); console.log(chalk.gray(` 3. Pull model: ollama pull ${options.model}`)); process.exit(1); } // Scan for PHP files spinner.text = 'Scanning PHP files...'; const files = await scanPhpFiles(targetPath); if (files.length === 0) { spinner.fail(chalk.red('No PHP files found')); process.exit(1); } // Filter to only class files const classFiles = files.filter((f) => { const info = extractClassInfo(f.content); return info.className !== null; }); if (classFiles.length === 0) { spinner.fail(chalk.red('No PHP classes found in the specified path')); process.exit(1); } spinner.succeed(chalk.green(`Found ${classFiles.length} PHP class(es)`)); // Ensure output directory exists const outputDir = resolve(options.output); await mkdir(outputDir, { recursive: true }); // Generate tests for each class const generated: GeneratedTest[] = []; for (let i = 0; i < classFiles.length; i++) { const file = classFiles[i]; const classInfo = extractClassInfo(file.content); const testFileName = `${classInfo.className}Test.php`; const fileSpinner = ora( `Generating tests for ${classInfo.className} (${i + 1}/${classFiles.length})...` ).start(); try { // Generate test code using AI let testCode = await ollama.generateTests(file.content, classInfo); // Clean up the response (remove markdown code blocks if present) testCode = cleanPhpCode(testCode); // Write test file const testPath = join(outputDir, testFileName); await writeFile(testPath, testCode, 'utf-8'); generated.push({ class: classInfo.className!, testFile: testPath, methods: classInfo.methods.length, }); fileSpinner.succeed(chalk.green(`Generated ${testFileName}`)); } catch (error) { const err = error as Error; fileSpinner.fail( chalk.red(`Failed to generate tests for ${classInfo.className}: ${err.message}`) ); } } // Summary if (generated.length === 0) { console.log(chalk.red('\n✗ No test files were generated.')); console.log(chalk.yellow('\nTry a different model:')); console.log(chalk.white(' ollama pull qwen2.5-coder')); console.log(chalk.white(` npx php-refactor test ${targetPath} -o ${options.output} --model qwen2.5-coder`)); } else { console.log(chalk.green(`\n✓ Generated ${generated.length} test file(s)`)); console.log(chalk.cyan('\nGenerated Tests:')); for (const item of generated) { console.log(chalk.gray(` • ${item.class}Test.php (${item.methods} methods)`)); } console.log(chalk.cyan('\nOutput directory:'), chalk.white(outputDir)); console.log(chalk.yellow('\nNext steps:')); console.log(chalk.gray(' 1. Review generated tests')); console.log(chalk.gray(' 2. Add missing assertions and edge cases')); console.log(chalk.gray(' 3. Run: vendor/bin/phpunit')); } } catch (error) { const err = error as Error; spinner.fail(chalk.red(`Error: ${err.message}`)); process.exit(1); } } /** * Clean PHP code from AI response * Removes markdown code blocks, intro text, and trailing notes * @param code - Raw AI response * @returns Clean PHP code * @throws Error if no valid PHP code found */ function cleanPhpCode(code: string): string { // Remove markdown code blocks let cleaned = code .replace(/```php\n?/gi, '') .replace(/```\n?/g, '') .trim(); // Find the first 0) { cleaned = cleaned.substring(phpTagIndex); } else if (phpTagIndex === -1) { // No