Files
pi_mcps/zoo_backup/home/tools/surefire_failures_summary.ts
T
2026-06-24 19:27:14 +02:00

145 lines
4.9 KiB
TypeScript

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)
}
},
})