From df0001f2104da324d98b1964fb475679ff81b6e8 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 29 Jul 2026 05:15:56 -0700 Subject: [PATCH] fix(test): restore main by catching #9813's expectations up to its own change main's CI has been red since #9813 (gate.ignoredCheckRuns) merged -- the last four ci.yml runs on main all failed, and every branch cut from or merged with main inherits it. #9804 went red on these tests with no changes of its own anywhere near them. #9813's implementation is correct; only the expectations were left behind: * fetchLiveCiAggregate / fetchLiveCiAggregatePreferGraphQl gained an eighth parameter (ignoredCheckRuns) while four toHaveBeenCalledWith assertions still passed seven; * the durable cache key gained an `|ign:` segment, while queue.test.ts's durable-cache regression still expected `|adv:` alone; * the whole ignoredCheckRuns block (19 lines) was added to .loopover.yml.example but not to config/examples/loopover.full.yml, which config-templates.test.ts requires byte-identical from `# WHERE IT LIVES` onward. No production behaviour changes. This matters beyond a red build: the gate auto-CLOSES a contributor PR on red CI, so a contributor who opened a PR after #9813 landed would be closed for a defect that is not theirs and that they cannot fix -- the same harm #9798 and #9801 describe, except this one does not self-correct. Closes #9828 --- config/examples/loopover.full.yml | 19 +++++++++++++++++++ test/unit/agent-approval-queue.test.ts | 6 +++--- test/unit/queue.test.ts | 6 ++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/config/examples/loopover.full.yml b/config/examples/loopover.full.yml index 13096355f..24056fc0c 100644 --- a/config/examples/loopover.full.yml +++ b/config/examples/loopover.full.yml @@ -290,6 +290,25 @@ gate: - name: Contributor trust appSlug: example-security-app + # Check-runs to IGNORE ENTIRELY (#9810) — the stronger sibling of advisoryCheckRuns above. Same + # spoof-resistant { name, appSlug } matching, but a matched run is treated as if it did not exist: it never + # gates CI, never counts as "still running", and — unlike advisory — never routes the PR to a manual-review + # hold either. Its conclusion is surfaced informationally only. + # + # Use this when a check's verdict carries no signal for YOUR repo while OTHER checks from the same app stay + # meaningful. The motivating case: a vendor app publishes both a real security scan AND a heuristic + # contributor-trust score. The scan is worth gating on; the trust score fails for perfectly good + # contributors, and listing it under advisoryCheckRuns still converts every one of their otherwise-clean PRs + # into a manual review — automation replaced by a queue of human decisions, and contributors left wondering + # whether they are being judged fairly. Ignoring the trust check keeps the scan's protection and drops the + # noise. If BOTH lists name the same check, ignore wins (it is the stronger, more explicit intent). + # + # List of { name, appSlug }, or omit. Default: not configured (byte-identical behavior for every repo that + # doesn't opt in). Config-as-code only — no DB column or dashboard toggle. + ignoredCheckRuns: + - name: Contributor trust + appSlug: example-security-app + # Promote a confident AI-judgment-only finding (one the reviewer itself placed under "Blockers", never # a "Nit") into a real, deterministic gate blocker instead of leaving it advisory (#3907). Only matters # for repos already running the registry content lane (see contentLane below) — content/registry repos diff --git a/test/unit/agent-approval-queue.test.ts b/test/unit/agent-approval-queue.test.ts index f38c30cf2..82be71643 100644 --- a/test/unit/agent-approval-queue.test.ts +++ b/test/unit/agent-approval-queue.test.ts @@ -583,7 +583,7 @@ describe("agent approval queue (#779)", () => { expect(result.status).toBe("accepted"); expect(fetchRequiredStatusContexts).toHaveBeenCalledWith(env, "owner/repo", null, expect.any(String), expect.any(String)); - expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["build", "test"]), expect.any(String), undefined); + expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["build", "test"]), expect.any(String), undefined, undefined); }); it("unions branch-protection contexts into the accept-time live CI re-check", async () => { @@ -599,7 +599,7 @@ describe("agent approval queue (#779)", () => { expect(result.status).toBe("accepted"); expect(fetchRequiredStatusContexts).toHaveBeenCalledWith(env, "owner/repo", "main", expect.any(String), expect.any(String)); - expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["branch-required", "build"]), expect.any(String), undefined); + expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["branch-required", "build"]), expect.any(String), undefined, undefined); }); it("falls back to expectedCiContexts when the accept-time branch-protection read fails", async () => { @@ -614,7 +614,7 @@ describe("agent approval queue (#779)", () => { const result = await decidePendingAgentAction(env, { id: action.id, decision: "accept", decidedBy: "owner" }); expect(result.status).toBe("accepted"); - expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["build"]), expect.any(String), null); + expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["build"]), expect.any(String), null, null); }); it("accept supersedes a staged merge when live CI has since turned pending, not just failed (#2126)", async () => { diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 54be4cf85..c82c44b2e 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -1877,6 +1877,7 @@ describe("queue processors", () => { new Set(["trusted-required-ci"]), "installation:9001", undefined, // #4372: advisoryCheckRuns (unconfigured here) + undefined, // #9813: ignoredCheckRuns (unconfigured here) ); expect(gateChecks).toBeGreaterThan(0); const finalized = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?") @@ -3168,8 +3169,9 @@ describe("queue processors", () => { await processJob(env, { type: "agent-regate-pr", deliveryId: "required-contexts-lookup-recovers", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); expect(liveCiSpy.mock.calls.length).toBeGreaterThan(liveReadsAfterFailedLookup); expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="miss"} 1'); - // #4372: the durable cache key now folds in the advisory-check-runs fingerprint (empty "|adv:" when unconfigured). - expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 7)).toMatchObject({ ciState: "passed", ciRequiredContextsKey: `${JSON.stringify(["trusted-required-ci"])}|adv:` }); + // #4372 + #9813: the durable cache key folds in the advisory-check-runs AND ignored-check-runs + // fingerprints (both empty here -- "|adv:|ign:" -- because neither is configured on this repo). + expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 7)).toMatchObject({ ciState: "passed", ciRequiredContextsKey: `${JSON.stringify(["trusted-required-ci"])}|adv:|ign:` }); } finally { liveCiSpy.mockRestore(); }