import path from 'node:path'; import fs from 'node:fs'; import { z } from 'zod'; import { config } from '../../config.js'; import { vendorBinExists } from '../../lib/frameworks/composer.js'; import { runCommand } from '../../lib/runCommand.js'; import { errorResult, jsonResult, toErrorMessage } from '../shared.js'; import { requireLaravel } from './requireLaravel.js'; function vendorBin(root, bin) { const suffix = process.platform === 'win32' ? '.bat' : ''; return path.join(root, 'vendor', 'bin', `${bin}${suffix}`); } function parsePhpUnitSummary(output) { // PHPUnit: "OK (12 tests, 34 assertions)" or "FAILURES!\nTests: 12, Assertions: 30, Failures: 2." const ok = output.match(/OK\s*\((\d+)\s+tests?,\s*(\d+)\s+assertions?\)/i); if (ok) { return { passed: true, tests: Number(ok[1]), assertions: Number(ok[2]), failures: 0, errors: 0 }; } const summary = output.match(/Tests:\s*(\d+),\s*Assertions:\s*(\d+)(?:,\s*Errors:\s*(\d+))?(?:,\s*Failures:\s*(\d+))?(?:,\s*Skipped:\s*(\d+))?/i); if (summary) { return { passed: !/FAILURES!|ERRORS!/i.test(output), tests: Number(summary[1]), assertions: Number(summary[2]), errors: Number(summary[3] ?? 0), failures: Number(summary[4] ?? 0), skipped: Number(summary[5] ?? 0), }; } // Pest often prints "Tests: 12 passed (34 assertions)" or similar. const pest = output.match(/Tests:\s+(\d+)\s+passed/i); if (pest) { return { passed: !/failed|FAIL/i.test(output), tests: Number(pest[1]) }; } return { passed: null, note: 'Could not parse a pass/fail summary from the runner output.' }; } export function registerPhpTestTools(server) { server.registerTool('run_phpunit', { title: 'Run PHPUnit', description: "Run the Laravel project's PHPUnit suite via vendor/bin/phpunit (or `php artisan test` as a fallback). Returns parsed pass/fail counts when possible.", inputSchema: { extraArgs: z .array(z.string()) .optional() .describe('Extra CLI args, e.g. ["--filter", "UserTest"].'), }, }, async ({ extraArgs }) => { try { const laravel = requireLaravel(); const args = extraArgs ?? []; let command; let cmdArgs; if (vendorBinExists(laravel.root, 'phpunit')) { command = vendorBin(laravel.root, 'phpunit'); cmdArgs = args; } else if (fs.existsSync(path.join(laravel.root, 'artisan'))) { command = 'php'; cmdArgs = ['artisan', 'test', '--without-tty', ...args]; } else { return errorResult(`No vendor/bin/phpunit found at ${laravel.root}. Run composer install (or run_install) first.`); } const result = await runCommand(command, cmdArgs, { cwd: laravel.root, timeoutMs: config.scaffoldCommandTimeoutMs, }); return jsonResult({ runner: 'phpunit', summary: parsePhpUnitSummary(result.output), ...result }); } catch (error) { return errorResult(toErrorMessage(error)); } }); server.registerTool('run_pest', { title: 'Run Pest', description: "Run the Laravel project's Pest suite via vendor/bin/pest. Falls back to an error if Pest is not installed.", inputSchema: { extraArgs: z .array(z.string()) .optional() .describe('Extra CLI args, e.g. ["--filter", "it creates a post"].'), }, }, async ({ extraArgs }) => { try { const laravel = requireLaravel(); if (!vendorBinExists(laravel.root, 'pest')) { return errorResult(`No vendor/bin/pest found at ${laravel.root}. Install pestphp/pest (composer_require) or use run_phpunit instead.`); } const result = await runCommand(vendorBin(laravel.root, 'pest'), extraArgs ?? [], { cwd: laravel.root, timeoutMs: config.scaffoldCommandTimeoutMs, }); return jsonResult({ runner: 'pest', summary: parsePhpUnitSummary(result.output), ...result }); } catch (error) { return errorResult(toErrorMessage(error)); } }); } //# sourceMappingURL=runPhpTests.js.map