import { parametersSchema as z, defineCustomTool, CustomToolContext } from "@roo-code/types" // @ts-ignore - Node built-ins import { readdirSync, readFileSync, statSync } from "fs" // @ts-ignore - Node built-ins import { join, resolve } from "path" export default defineCustomTool({ name: "surefire_failures_summary", description: "Find Maven surefire/failsafe XML reports and return a structured summary of ONLY the failed/errored tests. Saves reading dozens of XML files manually.", parameters: z.object({ projectRoot: z.string().describe("Repo root or module path (absolute or relative)"), module: z.string().optional().describe("Sub-path like 'java/modules/cs-modules/eau' to scope the search"), }), async execute({ projectRoot, module }, context: CustomToolContext) { try { // @ts-ignore - installed dependency const { XMLParser } = require("fast-xml-parser") const root = resolve(module ? join(projectRoot, module) : projectRoot) const reportFiles: string[] = [] // Recursive walk to find surefire report XMLs const walk = (dir: string, depth: number) => { if (depth > 12) return let entries: string[] try { entries = readdirSync(dir) } catch { return } for (const entry of entries) { if (entry === "node_modules" || entry === ".git") continue const full = join(dir, entry) try { const st = statSync(full) if (st.isDirectory()) { if (entry === "surefire-reports" || entry === "failsafe-reports") { // Collect TEST-*.xml from this directory const xmls = readdirSync(full).filter( (f: string) => f.startsWith("TEST-") && f.endsWith(".xml") ) for (const xml of xmls) { reportFiles.push(join(full, xml)) } } else { walk(full, depth + 1) } } } catch { // skip unreadable } } } walk(root, 0) if (reportFiles.length === 0) { return JSON.stringify({ reportsFound: 0, totalTests: 0, failed: 0, errors: 0, skipped: 0, failures: [], note: "No surefire/failsafe report XMLs found. Run 'mvn test' first.", }, null, 2) } // Filter to only recent reports (within 1 hour of the newest) const mtimes = reportFiles.map((f) => { try { return statSync(f).mtimeMs } catch { return 0 } }) const newest = Math.max(...mtimes) const cutoff = newest - 3600_000 // 1 hour const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", isArray: (name: string) => name === "testcase", }) let totalTests = 0 let totalFailed = 0 let totalErrors = 0 let totalSkipped = 0 let reportsProcessed = 0 const failures: any[] = [] for (let i = 0; i < reportFiles.length; i++) { if (mtimes[i] < cutoff) continue reportsProcessed++ const xml = readFileSync(reportFiles[i], "utf-8") let doc: any try { doc = parser.parse(xml) } catch { continue } const suite = doc.testsuite if (!suite) continue const tests = parseInt(suite["@_tests"] || "0", 10) const failed = parseInt(suite["@_failures"] || "0", 10) const errors = parseInt(suite["@_errors"] || "0", 10) const skipped = parseInt(suite["@_skipped"] || "0", 10) totalTests += tests totalFailed += failed totalErrors += errors totalSkipped += skipped if ((failed + errors) > 0 && suite.testcase) { for (const tc of suite.testcase) { const failure = tc.failure || tc.error if (!failure) continue const msg = typeof failure === "string" ? failure : (failure["#text"] || failure["@_message"] || "") const type = failure["@_type"] || "Unknown" const lines = msg.split("\n").filter((l: string) => l.trim()) failures.push({ testClass: tc["@_classname"] || suite["@_name"] || "", testName: tc["@_name"] || "", type, messageFirstLine: lines[0]?.substring(0, 200) || "", stackFirstLine: lines[1]?.substring(0, 200) || "", file: reportFiles[i], }) } } } return JSON.stringify({ reportsFound: reportsProcessed, totalTests, failed: totalFailed, errors: totalErrors, skipped: totalSkipped, failures: failures.slice(0, 50), // cap at 50 to prevent token explosion }, null, 2) } catch (err: any) { return JSON.stringify({ error: err.message ?? String(err) }, null, 2) } }, })