-
Notifications
You must be signed in to change notification settings - Fork 562
Render grader values in collapsible step summary #55823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -460,10 +460,40 @@ function evaluateThreshold(value, direction, threshold) { | |
| function sanitizeSummaryText(value) { | ||
| return String(value ?? "") | ||
| .replace(/\r?\n/g, " ") | ||
| .replace(/\\/g, "\\\\") | ||
| .replace(/\|/g, "\\|") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Markdown escaping here is incomplete: if a cell already contains a backslash before a pipe (for example 💡 Why this mattersThis helper is meant to harden untrusted grader names/sources/units before putting them into a Markdown table. Right now it only handles the simple Escape backslashes before pipes (or avoid Markdown tables entirely for untrusted content), and add a regression test for an input like return String(value ?? "")
.replace(/\\/g, "\\\\")
.replace(/\|/g, "\\|")
.replace((r/redacted)?\n/g, " ")
... |
||
| .replace(/[<>&]/g, ch => (ch === "<" ? "<" : ch === ">" ? ">" : "&")) | ||
| .trim(); | ||
| } | ||
|
|
||
| /** | ||
| * @param {GraderResult} result | ||
| * @returns {result is GraderResult & {value: number}} | ||
| */ | ||
| function hasComputedValue(result) { | ||
| return typeof result.value === "number"; | ||
| } | ||
|
|
||
| /** | ||
| * Build the Graders section body for the GitHub Actions step summary. | ||
| * @param {GraderResult[]} results | ||
| * @returns {string} | ||
| */ | ||
| function buildGradersSummaryBody(results) { | ||
| const computedResults = results.filter(hasComputedValue); | ||
| if (computedResults.length === 0) { | ||
| return "\n\nNo grader values available.\n\n"; | ||
| } | ||
|
|
||
| const statusLabels = { pass: "Pass", fail: "Fail", error: "Error", unavailable: "Unavailable" }; | ||
| const rows = computedResults.map(result => { | ||
| const value = String(Number(result.value.toFixed(4))); | ||
| return `| ${statusLabels[result.status] || "Unknown"} | ${sanitizeSummaryText(result.name)} | ${sanitizeSummaryText(result.source)} | ${value} | ${sanitizeSummaryText(result.unit || "—")} |`; | ||
| }); | ||
|
|
||
| return ["", "", "| Status | Grader | Source | Value | Unit |", "| --- | --- | --- | --- | --- |", ...rows, "", ""].join("\n"); | ||
| } | ||
|
|
||
| /** | ||
| * Normalize a grader result from either built-in number or custom object return. | ||
| * @param {string} id | ||
|
|
@@ -806,25 +836,7 @@ async function main(manifestB64, execSpecB64) { | |
| } | ||
|
|
||
| // Step summary | ||
| core.summary.addHeading("Graders", 3); | ||
| const tableResults = results.filter(r => r.status !== "unavailable"); | ||
| if (tableResults.length > 0) { | ||
| const rows = tableResults.map(r => { | ||
| const statusIcon = r.status === "pass" ? "✅" : r.status === "fail" ? "❌" : "⚠️"; | ||
| const val = r.value !== null ? String(Number(r.value.toFixed(4))) : "—"; | ||
| return [statusIcon, sanitizeSummaryText(r.name), r.source, val, sanitizeSummaryText(r.unit || "—")]; | ||
| }); | ||
| core.summary.addTable([ | ||
| [ | ||
| { data: "", header: true }, | ||
| { data: "Grader", header: true }, | ||
| { data: "Source", header: true }, | ||
| { data: "Value", header: true }, | ||
| { data: "Unit", header: true }, | ||
| ], | ||
| ...rows, | ||
| ]); | ||
| } | ||
| core.summary.addDetails("Graders", buildGradersSummaryBody(results)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This summary table is still broken in the step summary: 💡 Why this mattersThe previous A safe fix is to wrap the generated body with blank lines, or build the details block manually: core.summary
.addRaw("<details><summary>Graders</summary>\n\n")
.addRaw(buildGradersSummaryBody(results))
.addRaw("\n\n</details>\n");Alternatively, make |
||
| const errResults = results.filter(r => r.error); | ||
| if (errResults.length > 0) { | ||
| const errLines = errResults.map(r => `- **${sanitizeSummaryText(r.id)}**: runtime error (see step logs)`).join("\n"); | ||
|
|
@@ -850,6 +862,7 @@ module.exports = { | |
| runCustomGrader, | ||
| runOperationalValueGrader, | ||
| normalizeResult, | ||
| buildGradersSummaryBody, | ||
| evaluateThreshold, | ||
| BUILTIN_GRADERS, | ||
| BUILTIN_META, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ const { | |
| runBuiltinGrader, | ||
| runCustomGrader, | ||
| normalizeResult, | ||
| buildGradersSummaryBody, | ||
| evaluateThreshold, | ||
| BUILTIN_GRADERS, | ||
| BUILTIN_META, | ||
|
|
@@ -68,6 +69,42 @@ function makeTrace(overrides = {}) { | |
| } | ||
|
|
||
| describe("trace_graders", () => { | ||
| describe("buildGradersSummaryBody", () => { | ||
| it("renders all computed grader values without emojis", () => { | ||
| const summary = buildGradersSummaryBody([ | ||
| { id: "tool-success-rate", name: "Tool success rate", value: 0.98765, unit: "ratio", status: "pass", source: "builtin" }, | ||
| { id: "custom", name: "Custom", value: 2, unit: "count", status: "fail", source: "inline" }, | ||
| { id: "unavailable", name: "Unavailable", value: null, unit: "", status: "unavailable", source: "builtin" }, | ||
| ]); | ||
|
|
||
| expect(summary).toContain("| Pass | Tool success rate | builtin | 0.9877 | ratio |"); | ||
| expect(summary).toContain("| Fail | Custom | inline | 2 | count |"); | ||
| expect(summary).not.toContain("Unavailable"); | ||
| expect(summary).not.toMatch(/[\u{1F300}-\u{1FAFF}]/u); | ||
| }); | ||
|
|
||
| it("escapes untrusted table cells", () => { | ||
| const summary = buildGradersSummaryBody([{ id: "custom", name: "Custom | <grader>", value: 1, unit: "unit|&", status: "pass", source: "inline|source" }]); | ||
|
|
||
| expect(summary).toContain("Custom \\| <grader>"); | ||
| expect(summary).toContain("inline\\|source"); | ||
| expect(summary).toContain("unit\\|&"); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] Missing test for the empty-results branch — 💡 Suggested testit("returns a message when no grader has a computed value", () => {
const summary = buildGradersSummaryBody([
{ id: "no-value", name: "No value", value: null, unit: "", status: "unavailable", source: "builtin" },
]);
expect(summary).toBe("No grader values available.");
});Without this test the fallback message can silently regress. @copilot please address this. |
||
|
|
||
| it("escapes backslashes before pipes", () => { | ||
| const summary = buildGradersSummaryBody([{ id: "custom", name: "A\\|B", value: 1, unit: "count", status: "pass", source: "inline" }]); | ||
|
|
||
| expect(summary).toContain("| Pass | A\\\\\\|B | inline | 1 | count |"); | ||
| }); | ||
|
|
||
| it("surrounds the table with blank lines for details rendering", () => { | ||
| const summary = buildGradersSummaryBody([{ id: "custom", name: "Custom", value: 1, unit: "count", status: "pass", source: "inline" }]); | ||
|
|
||
| expect(summary.startsWith("\n\n")).toBe(true); | ||
| expect(summary.endsWith("\n\n")).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("archiveOperationalValueEvaluator", () => { | ||
| it("writes only evaluator bytes matching the frozen digest", () => { | ||
| const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "operational-value-evaluator-archive-")); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.