From 16ccd5f586f8921d48581f022b2fae25ea34c1a2 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:53:58 -0300 Subject: [PATCH] chore(ci): pr-evidence FAIL output tells you to push (body edit does not re-run the gate) (#5944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ci.yml ignores the 'edited' event, so adding the Evidence block to the PR body after a push does not re-run check:pr-evidence — you need another commit. The FAIL report now says so, at the exact place someone sees the red check. + 5 unit tests (classification + hint-on-fail / no-hint-on-pass). Decided against a separate edited-triggered workflow: pr-evidence is not a required check (no ruleset gates it; release PRs merge UNSTABLE, not BLOCKED), so the gap is cosmetic and the generate-release skill already puts Evidence in the body before the first push. --- scripts/check/check-pr-evidence.mjs | 11 +++++- tests/unit/check-pr-evidence.test.ts | 55 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/unit/check-pr-evidence.test.ts diff --git a/scripts/check/check-pr-evidence.mjs b/scripts/check/check-pr-evidence.mjs index f5cbbf8624..41f059d67f 100644 --- a/scripts/check/check-pr-evidence.mjs +++ b/scripts/check/check-pr-evidence.mjs @@ -231,7 +231,16 @@ if (isMain) { } else if (result === "pass") { reportLines.push("Result: PASS", "", reason); } else { - reportLines.push("Result: FAIL", "", reason); + reportLines.push( + "Result: FAIL", + "", + reason, + "", + "> ℹ️ Editing the PR body to add the evidence does NOT re-run this gate — `ci.yml` " + + "does not listen to the `edited` event. Add the `## Evidence` block, then **push a " + + "commit** (or re-run this job) to re-validate. For releases, put the Evidence block in " + + "the body BEFORE the first push (see the generate-release skill, Phase 0)." + ); } const report = buildReport(reportLines); diff --git a/tests/unit/check-pr-evidence.test.ts b/tests/unit/check-pr-evidence.test.ts new file mode 100644 index 0000000000..3125ac1fe3 --- /dev/null +++ b/tests/unit/check-pr-evidence.test.ts @@ -0,0 +1,55 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const mod = await import("../../scripts/check/check-pr-evidence.mjs"); +const { evaluatePrBody } = mod; +const SCRIPT = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../scripts/check/check-pr-evidence.mjs" +); + +function run(body) { + try { + const out = execFileSync("node", [SCRIPT], { + encoding: "utf8", + env: { ...process.env, PR_BODY: body }, + }); + return { code: 0, out }; + } catch (err) { + return { code: err.status ?? 1, out: `${err.stdout || ""}${err.stderr || ""}` }; + } +} + +test("evaluatePrBody: no outcome claim → pass (no evidence required)", () => { + const r = evaluatePrBody("Adds a helper module."); + assert.equal(r.result, "pass"); + assert.match(r.reason, /no evidence required/i); +}); + +test("evaluatePrBody: outcome claim + evidence block → pass", () => { + const r = evaluatePrBody("Tests pass.\n\n## Evidence\n```\ntests 20 / pass 20 / fail 0\n```"); + assert.equal(r.result, "pass"); +}); + +test("evaluatePrBody: outcome claim without evidence → fail", () => { + const r = evaluatePrBody("All 20 tests pass and 0 errors."); + assert.equal(r.result, "fail"); +}); + +test("the FAIL report explains that editing the body does not re-run the gate (push instead)", () => { + const { code, out } = run("All 20 tests pass and 0 errors."); // claim, no evidence + assert.equal(code, 1, "gate fails on a claim with no evidence"); + assert.match(out, /Result: FAIL/); + assert.match(out, /does NOT re-run this gate/); + assert.match(out, /push a commit/i); +}); + +test("the hint does NOT appear when the gate passes", () => { + const { code, out } = run("Tests pass.\n\n## Evidence\n```\ntests 20 / pass 20 / fail 0\n```"); + assert.equal(code, 0); + assert.match(out, /Result: PASS/); + assert.doesNotMatch(out, /does NOT re-run this gate/); +});