import { parametersSchema as z, defineCustomTool } from "@roo-code/types" import { spawnSync } from "child_process" import { readdirSync, readFileSync, existsSync } from "fs" import { join } from "path" export default defineCustomTool({ name: "mvn_test", description: "Run Maven tests for a module in a multi-module project. Parses surefire XML reports and returns structured pass/fail results.", parameters: z.object({ worktreePath: z.string().describe("Path to the project root or worktree (e.g. /path/to/my-project)"), module: z.string().describe("Module path relative to java/, e.g. modules/cs-modules/my-module"), testClass: z.string().optional().describe("Specific test class to run, e.g. MyServiceTest"), }), async execute({ worktreePath, module, testClass }) { const pomPath = join(worktreePath, "java", "pom.xml") if (!existsSync(pomPath)) { return `Error: pom.xml not found at ${pomPath}` } const args = ["test", "-pl", `java/${module}`, "-f", pomPath, "--batch-mode", "-q"] if (testClass) { args.push(`-Dtest=${testClass}`) } const result = spawnSync("mvn", args, { encoding: "utf-8", timeout: 300000, cwd: worktreePath, maxBuffer: 10 * 1024 * 1024, }) const surefireDir = join(worktreePath, "java", module, "target", "surefire-reports") const summary = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0, failures: [] as string[] } if (existsSync(surefireDir)) { try { const xmlFiles = readdirSync(surefireDir).filter(f => f.startsWith("TEST-") && f.endsWith(".xml")) for (const xmlFile of xmlFiles) { const content = readFileSync(join(surefireDir, xmlFile), "utf-8") const testsMatch = content.match(/tests="(\d+)"/) const failuresMatch = content.match(/failures="(\d+)"/) const errorsMatch = content.match(/errors="(\d+)"/) const skippedMatch = content.match(/skipped="(\d+)"/) if (testsMatch) summary.total += parseInt(testsMatch[1]) if (failuresMatch) summary.failed += parseInt(failuresMatch[1]) if (errorsMatch) summary.errors += parseInt(errorsMatch[1]) if (skippedMatch) summary.skipped += parseInt(skippedMatch[1]) const failureMatches = content.matchAll(/]*message="([^"]*)"[^>]*>/g) for (const m of failureMatches) { summary.failures.push(`${xmlFile.replace("TEST-", "").replace(".xml", "")}: ${m[1]}`) } } summary.passed = summary.total - summary.failed - summary.errors - summary.skipped } catch (e) { // surefire parsing failed, continue with what we have } } return JSON.stringify({ buildStatus: result.status === 0 ? "SUCCESS" : "FAILURE", exitCode: result.status, summary, lastOutput: (result.status !== 0 ? (result.stderr || result.stdout || "") : "").split("\n").slice(-15).join("\n"), }, null, 2) }, })