From 8c83705258b62f0fa91ae477c2f1729b33948f9e Mon Sep 17 00:00:00 2001 From: BigSimmo Date: Tue, 13 Oct 2026 20:55:08 +0800 Subject: [PATCH 1/5] CI: make the tree-proof --self-test exit non-zero when an assertion fails --- scripts/ci-main-tree-proof.mjs | 13 ++++++++++++- tests/ci-main-tree-proof.test.ts | 11 +++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/scripts/ci-main-tree-proof.mjs b/scripts/ci-main-tree-proof.mjs index 397c8958e8..e6533d8011 100644 --- a/scripts/ci-main-tree-proof.mjs +++ b/scripts/ci-main-tree-proof.mjs @@ -494,6 +494,7 @@ function assert(condition, message) { } async function selfTest() { + if (process.env.CI_TREE_PROOF_SELF_TEST_FORCE_FAIL === "1") assert(false, "forced failure (test hook)"); const ok = (name, extra = {}) => ({ name, status: "completed", conclusion: "success", ...extra }); const full = [ { ...ok("Change scope"), id: 501 }, @@ -775,7 +776,17 @@ function recordTestedTree({ env = process.env, runGit = git } = {}) { } async function main() { - if (process.argv.includes("--self-test")) return selfTest(); + if (process.argv.includes("--self-test")) { + // A self-test failure must fail the process. The fail-open handler below exists for the CI + // proof path only; applied here it would print a warning and exit 0, hiding broken guards. + try { + await selfTest(); + } catch (error) { + console.error(`ci-main-tree-proof self-test FAILED: ${String(error?.stack ?? error)}`); + process.exitCode = 1; + } + return; + } if (process.argv.includes("--record-tested-tree")) { try { recordTestedTree(); diff --git a/tests/ci-main-tree-proof.test.ts b/tests/ci-main-tree-proof.test.ts index 8c82ebe49a..9860dd7d78 100644 --- a/tests/ci-main-tree-proof.test.ts +++ b/tests/ci-main-tree-proof.test.ts @@ -48,6 +48,17 @@ describe("main tree proof script", () => { expect(result.stdout).toContain("ci-main-tree-proof self-test passed."); }); + it("exits non-zero when a self-test assertion fails", () => { + const result = spawnSync(process.execPath, ["scripts/ci-main-tree-proof.mjs", "--self-test"], { + encoding: "utf8", + env: { ...process.env, CI_TREE_PROOF_SELF_TEST_FORCE_FAIL: "1" }, + }); + expect(result.status).toBe(1); + expect(result.stderr).toContain("self-test FAILED"); + expect(result.stdout).not.toContain("self-test passed"); + expect(result.stdout).not.toContain("coverage="); + }); + it("names exactly the PR-run jobs each group skips, matching ci.yml job names", () => { const names = Object.values(jobs).map((job) => String((job as { name?: string }).name ?? "")); for (const name of [ From 231a402bd478e3455a6200cdf635ae51d654c9d7 Mon Sep 17 00:00:00 2001 From: BigSimmo Date: Tue, 13 Oct 2026 21:54:13 +0800 Subject: [PATCH 2/5] Test: a failed tree-proof self-test leaves GITHUB_OUTPUT and the step summary empty --- tests/ci-main-tree-proof.test.ts | 34 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/tests/ci-main-tree-proof.test.ts b/tests/ci-main-tree-proof.test.ts index 9860dd7d78..39c6222338 100644 --- a/tests/ci-main-tree-proof.test.ts +++ b/tests/ci-main-tree-proof.test.ts @@ -48,15 +48,31 @@ describe("main tree proof script", () => { expect(result.stdout).toContain("ci-main-tree-proof self-test passed."); }); - it("exits non-zero when a self-test assertion fails", () => { - const result = spawnSync(process.execPath, ["scripts/ci-main-tree-proof.mjs", "--self-test"], { - encoding: "utf8", - env: { ...process.env, CI_TREE_PROOF_SELF_TEST_FORCE_FAIL: "1" }, - }); - expect(result.status).toBe(1); - expect(result.stderr).toContain("self-test FAILED"); - expect(result.stdout).not.toContain("self-test passed"); - expect(result.stdout).not.toContain("coverage="); + it("exits non-zero when a self-test assertion fails, writing no GitHub outputs", () => { + const outDir = mkdtempSync(path.join(tmpdir(), "tree-proof-self-test-")); + const outputFile = path.join(outDir, "output"); + const summaryFile = path.join(outDir, "summary"); + writeFileSync(outputFile, ""); + writeFileSync(summaryFile, ""); + try { + const result = spawnSync(process.execPath, ["scripts/ci-main-tree-proof.mjs", "--self-test"], { + encoding: "utf8", + env: { + ...process.env, + CI_TREE_PROOF_SELF_TEST_FORCE_FAIL: "1", + GITHUB_OUTPUT: outputFile, + GITHUB_STEP_SUMMARY: summaryFile, + }, + }); + expect(result.status).toBe(1); + expect(result.stderr).toContain("self-test FAILED"); + expect(result.stdout).not.toContain("self-test passed"); + expect(result.stdout).not.toContain("coverage="); + expect(readFileSync(outputFile, "utf8")).toBe(""); + expect(readFileSync(summaryFile, "utf8")).toBe(""); + } finally { + rmSync(outDir, { recursive: true, force: true }); + } }); it("names exactly the PR-run jobs each group skips, matching ci.yml job names", () => { From f7ecf07a4f042287f4bb5db1fecbb033d259e38f Mon Sep 17 00:00:00 2001 From: BigSimmo Date: Tue, 13 Oct 2026 22:56:43 +0800 Subject: [PATCH 3/5] test: bound retries on tree-proof self-test cleanup (fixes test-runner-safety ratchet) --- tests/ci-main-tree-proof.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci-main-tree-proof.test.ts b/tests/ci-main-tree-proof.test.ts index 39c6222338..c07ea63a85 100644 --- a/tests/ci-main-tree-proof.test.ts +++ b/tests/ci-main-tree-proof.test.ts @@ -71,7 +71,7 @@ describe("main tree proof script", () => { expect(readFileSync(outputFile, "utf8")).toBe(""); expect(readFileSync(summaryFile, "utf8")).toBe(""); } finally { - rmSync(outDir, { recursive: true, force: true }); + rmSync(outDir, { recursive: true, force: true, maxRetries: 5 }); } }); From 6f05c18dfae3e6e683b9ee1b7477f0adf5df8d76 Mon Sep 17 00:00:00 2001 From: BigSimmo Date: Tue, 13 Oct 2026 23:06:21 +0800 Subject: [PATCH 4/5] test: require a stack frame in the forced self-test failure output --- tests/ci-main-tree-proof.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ci-main-tree-proof.test.ts b/tests/ci-main-tree-proof.test.ts index c07ea63a85..b8090098d5 100644 --- a/tests/ci-main-tree-proof.test.ts +++ b/tests/ci-main-tree-proof.test.ts @@ -66,6 +66,7 @@ describe("main tree proof script", () => { }); expect(result.status).toBe(1); expect(result.stderr).toContain("self-test FAILED"); + expect(result.stderr).toMatch(/forced failure \(test hook\)[\r\n]\s+at /); expect(result.stdout).not.toContain("self-test passed"); expect(result.stdout).not.toContain("coverage="); expect(readFileSync(outputFile, "utf8")).toBe(""); From 1b729e89761958c4c39eaf0962a9a91e73b646c3 Mon Sep 17 00:00:00 2001 From: BigSimmo Date: Tue, 13 Oct 2026 23:06:34 +0800 Subject: [PATCH 5/5] style: format ci-main-tree-proof test --- tests/ci-main-tree-proof.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ci-main-tree-proof.test.ts b/tests/ci-main-tree-proof.test.ts index b8090098d5..9554e74b5b 100644 --- a/tests/ci-main-tree-proof.test.ts +++ b/tests/ci-main-tree-proof.test.ts @@ -66,7 +66,7 @@ describe("main tree proof script", () => { }); expect(result.status).toBe(1); expect(result.stderr).toContain("self-test FAILED"); - expect(result.stderr).toMatch(/forced failure \(test hook\)[\r\n]\s+at /); + expect(result.stderr).toMatch(/forced failure \(test hook\)[\r\n]\s+at /); expect(result.stdout).not.toContain("self-test passed"); expect(result.stdout).not.toContain("coverage="); expect(readFileSync(outputFile, "utf8")).toBe("");