145 lines
4.9 KiB
TypeScript
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)
|
|
}
|
|
},
|
|
})
|