{"version":3,"file":"report-junit.mjs","names":[],"sources":["../../../../../../../ai/src/eval/report-junit.ts"],"sourcesContent":["import type { EvalCaseResult, EvalReport } from \"../contracts/agent/eval.type\";\n\n/**\n * Escape the five XML predefined entities so arbitrary text (case names,\n * failure reasons, agent names) is safe inside an attribute value or\n * element body. Covers `&`, `<`, `>`, `\"`, and `'`.\n */\nfunction escapeXml(value: string): string {\n  return value\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\")\n    .replace(/\"/g, \"&quot;\")\n    .replace(/'/g, \"&apos;\");\n}\n\n/**\n * Build the `<failure>` body for a failed case: the joined reasons of\n * every non-passing scorer, falling back to a generic message when a\n * scorer offered no reason (or the failure was an agent error).\n */\nfunction failureMessage(entry: EvalCaseResult): string {\n  if (entry.result.error) {\n    return `agent error: ${entry.result.error.message}`;\n  }\n\n  const reasons = entry.scores\n    .filter((score) => score.passed === false)\n    .map((score) => score.reason)\n    .filter((reason): reason is string => typeof reason === \"string\" && reason !== \"\");\n\n  if (reasons.length > 0) {\n    return reasons.join(\"; \");\n  }\n\n  return \"case did not pass\";\n}\n\n/**\n * Serialize an {@link EvalReport} to a JUnit-XML string for CI ingestion.\n *\n * **Role.** A pure, runner-decoupled reporter: one `<testsuite>` whose\n * name is the agent, one `<testcase>` per eval case, a `<failure>` child\n * on each case that did not pass (with the joined scorer reasons), and a\n * `time` attribute carrying the case / suite duration in **seconds**\n * (JUnit's unit; the report stores milliseconds).\n *\n * XML is hand-emitted (no `xml` dependency) and every dynamic value is\n * entity-escaped via {@link escapeXml}.\n *\n * @example\n * await writeFile(\"./report.junit.xml\", toJUnit(report));\n */\nexport function toJUnit(report: EvalReport): string {\n  const suiteName = escapeXml(report.agentName);\n  const suiteTime = (report.duration / 1000).toFixed(3);\n\n  const lines: string[] = [];\n\n  lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n  lines.push(\n    `<testsuite name=\"${suiteName}\" tests=\"${report.total}\" failures=\"${report.failedCount}\" time=\"${suiteTime}\">`,\n  );\n\n  for (const entry of report.cases) {\n    const caseName = escapeXml(entry.case.name);\n    const caseTime = (entry.duration / 1000).toFixed(3);\n\n    if (entry.passed) {\n      lines.push(\n        `  <testcase name=\"${caseName}\" classname=\"${suiteName}\" time=\"${caseTime}\"/>`,\n      );\n\n      continue;\n    }\n\n    const message = failureMessage(entry);\n    lines.push(\n      `  <testcase name=\"${caseName}\" classname=\"${suiteName}\" time=\"${caseTime}\">`,\n    );\n    lines.push(\n      `    <failure message=\"${escapeXml(message)}\">${escapeXml(message)}</failure>`,\n    );\n    lines.push(\"  </testcase>\");\n  }\n\n  lines.push(\"</testsuite>\");\n\n  return lines.join(\"\\n\");\n}\n"],"mappings":";;;;;;AAOA,SAAS,UAAU,OAAuB;CACxC,OAAO,MACJ,QAAQ,MAAM,OAAO,CAAC,CACtB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,QAAQ,CAAC,CACvB,QAAQ,MAAM,QAAQ;AAC3B;;;;;;AAOA,SAAS,eAAe,OAA+B;CACrD,IAAI,MAAM,OAAO,OACf,OAAO,gBAAgB,MAAM,OAAO,MAAM;CAG5C,MAAM,UAAU,MAAM,OACnB,QAAQ,UAAU,MAAM,WAAW,KAAK,CAAC,CACzC,KAAK,UAAU,MAAM,MAAM,CAAC,CAC5B,QAAQ,WAA6B,OAAO,WAAW,YAAY,WAAW,EAAE;CAEnF,IAAI,QAAQ,SAAS,GACnB,OAAO,QAAQ,KAAK,IAAI;CAG1B,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAgB,QAAQ,QAA4B;CAClD,MAAM,YAAY,UAAU,OAAO,SAAS;CAC5C,MAAM,aAAa,OAAO,WAAW,IAAI,CAAE,QAAQ,CAAC;CAEpD,MAAM,QAAkB,CAAC;CAEzB,MAAM,KAAK,4CAAwC;CACnD,MAAM,KACJ,oBAAoB,UAAU,WAAW,OAAO,MAAM,cAAc,OAAO,YAAY,UAAU,UAAU,GAC7G;CAEA,KAAK,MAAM,SAAS,OAAO,OAAO;EAChC,MAAM,WAAW,UAAU,MAAM,KAAK,IAAI;EAC1C,MAAM,YAAY,MAAM,WAAW,IAAI,CAAE,QAAQ,CAAC;EAElD,IAAI,MAAM,QAAQ;GAChB,MAAM,KACJ,qBAAqB,SAAS,eAAe,UAAU,UAAU,SAAS,IAC5E;GAEA;EACF;EAEA,MAAM,UAAU,eAAe,KAAK;EACpC,MAAM,KACJ,qBAAqB,SAAS,eAAe,UAAU,UAAU,SAAS,GAC5E;EACA,MAAM,KACJ,yBAAyB,UAAU,OAAO,EAAE,IAAI,UAAU,OAAO,EAAE,WACrE;EACA,MAAM,KAAK,eAAe;CAC5B;CAEA,MAAM,KAAK,cAAc;CAEzB,OAAO,MAAM,KAAK,IAAI;AACxB"}