Skip to content

Commit ecfe2ee

Browse files
naimish-browseclaude
authored andcommitted
feat: mark each outcome, so the heading reads at a glance
✅ Succeeded ❌ Tests failed 💥 Run broke 🛑 Stopped at limit ⚠️ Could not run ⏭️ Skipped In front of the words, never instead of them, so a screen reader still gets the whole outcome. An unknown outcome gets no mark rather than a crash. 611 tests pass (6 new). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ce1dc34 commit ecfe2ee

4 files changed

Lines changed: 92 additions & 3 deletions

File tree

‎test-companion/src/outcome.mjs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,32 @@ export function checkConclusion(outcome) {
128128
}
129129
}
130130

131+
/**
132+
* One mark per outcome, so the heading reads at a glance.
133+
*
134+
* "## Test Companion — Run broke" and "## Test Companion — Succeeded" look the
135+
* same in a crowded pull request thread, and the one that needs attention is
136+
* the one that blends in.
137+
*
138+
* Deliberately the four GitHub itself uses for the same ideas — a tick for
139+
* done, a cross for a failing test, a warning for "somebody has to look" —
140+
* rather than anything decorative. An outcome this Action has never heard of,
141+
* which a newer CLI could report, gets no mark rather than a crash: losing the
142+
* mark is cosmetic, losing the report is not.
143+
*/
144+
const OUTCOME_MARKS = Object.freeze({
145+
[OUTCOMES.SUCCEEDED]: "✅",
146+
[OUTCOMES.TESTS_FAILED]: "❌",
147+
[OUTCOMES.RUN_BROKE]: "💥",
148+
[OUTCOMES.STOPPED_AT_LIMIT]: "🛑",
149+
[OUTCOMES.COULD_NOT_RUN]: "⚠️",
150+
[OUTCOMES.SKIPPED]: "⏭️",
151+
})
152+
153+
export function emojiFor(outcome) {
154+
return OUTCOME_MARKS[outcome] ?? ""
155+
}
156+
131157
/** What a repository can ask a bad outcome to do to its build — FR-9. */
132158
export const BUILD_EFFECTS = Object.freeze({
133159
/** Never red. The check still reports truthfully. */

‎test-companion/src/report.mjs‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88
* preinstalled on every runner and already how this product talks to GitHub.
99
*/
1010
import { CAPABILITIES } from "./intent.mjs"
11+
import { emojiFor } from "./outcome.mjs"
12+
13+
/**
14+
* The outcome with its mark in front — never instead of the words, so a reader
15+
* with images off, or a screen reader, still gets the whole outcome. An
16+
* outcome with no mark reads exactly as it did before.
17+
*/
18+
const withMark = (outcome) => {
19+
const mark = emojiFor(outcome)
20+
return mark ? `${mark} ${outcome}` : String(outcome ?? "")
21+
}
1122

1223
/**
1324
* How a later run finds the comment it wrote earlier — FR-29 wants ONE comment,
@@ -140,7 +151,9 @@ export function renderJobSummary({
140151
warnings = [],
141152
} = {}) {
142153
const lines = []
143-
lines.push(`## Test Companion — ${outcome}`)
154+
// The mark goes in front of the words, never instead of them: a reader with
155+
// images off, or a screen reader, still gets the whole outcome.
156+
lines.push(`## Test Companion — ${withMark(outcome)}`)
144157
lines.push("")
145158

146159
// FR-43: both asking and approving need write access, so one person doing
@@ -176,7 +189,7 @@ export function renderJobSummary({
176189
const rows = [
177190
["Job", workflow ?? summary.workflow],
178191
["Trigger", summary.event],
179-
["Outcome", outcome],
192+
["Outcome", withMark(outcome)],
180193
["Took", summary.duration],
181194
// Credits, not dollars: it is the unit the bill and the pipeline file's
182195
// own ceiling are written in, so a figure in dollars would not line up
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { test, describe } from "node:test"
2+
import assert from "node:assert/strict"
3+
import { OUTCOMES, emojiFor } from "../src/outcome.mjs"
4+
import { renderJobSummary } from "../src/report.mjs"
5+
6+
/**
7+
* The outcome should be readable at a glance.
8+
*
9+
* "## Test Companion — Run broke" and "## Test Companion — Succeeded" look the
10+
* same in a crowded pull request thread, and the one that needs attention is
11+
* the one that blends in.
12+
*/
13+
14+
describe("every outcome has a mark of its own", () => {
15+
test("no two outcomes share one", () => {
16+
const marks = Object.values(OUTCOMES).map(emojiFor)
17+
assert.equal(new Set(marks).size, marks.length, `marks collide: ${marks.join(" ")}`)
18+
})
19+
20+
test("every outcome has one", () => {
21+
for (const o of Object.values(OUTCOMES)) {
22+
assert.ok(emojiFor(o), `${o} has no mark`)
23+
}
24+
})
25+
26+
test("an outcome we do not know does not break the heading", () => {
27+
// A newer CLI could report something this Action has never heard of.
28+
// A missing mark is a cosmetic loss; a crash is a lost report.
29+
assert.equal(emojiFor("Something new"), "")
30+
assert.equal(emojiFor(undefined), "")
31+
})
32+
33+
test("the heading carries it", () => {
34+
const body = renderJobSummary({ workflow: "fix", outcome: OUTCOMES.RUN_BROKE, summary: {}, runUrl: "https://run" })
35+
assert.match(body.split("\n")[0], /^## Test Companion — .+ Run broke$/u)
36+
assert.ok(body.split("\n")[0].includes(emojiFor(OUTCOMES.RUN_BROKE)))
37+
})
38+
39+
test("and so does the row, so the table reads on its own", () => {
40+
const body = renderJobSummary({ workflow: "fix", outcome: OUTCOMES.TESTS_FAILED, summary: {}, runUrl: "https://run" })
41+
assert.match(body, new RegExp(`\\| Outcome \\| ${emojiFor(OUTCOMES.TESTS_FAILED)} Tests failed \\|`, "u"))
42+
})
43+
44+
test("a success still reads as a success", () => {
45+
const body = renderJobSummary({ workflow: "generate", outcome: OUTCOMES.SUCCEEDED, summary: {}, runUrl: "https://run" })
46+
assert.match(body.split("\n")[0], /Succeeded$/)
47+
})
48+
})

‎test-companion/test/report.test.mjs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1191,7 +1191,9 @@ describe("renderComment keeps the body postable", () => {
11911191
test("the table still renders above the cut answer", () => {
11921192
const body = renderComment(view("word ".repeat(40_000)))
11931193
assert.ok(body.includes("| Job | fix |"))
1194-
assert.ok(body.includes("Test Companion — Succeeded"))
1194+
// The heading carries a mark in front of the words now, so match the
1195+
// shape rather than the exact string — see outcome-emoji.test.mjs.
1196+
assert.match(body, /## Test Companion — .*Succeeded/u)
11951197
})
11961198

11971199
// Not the answer's fault. The listed failures are capped, but a test NAME

0 commit comments

Comments
 (0)