From 3ec66aea9fac90c5a6f50a81db7d5c5122ebc85c Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:41:00 -0700 Subject: [PATCH 1/2] fix(test): carry #9813's new argument and cache-key fragment into the six stale assertions (#9822) `main` is red on its own: six tests fail at 9ec808c47 with no PR involved, so every PR opened against it inherits a `validate` failure for work that is correct. #9813 (`gate.ignoredCheckRuns`) made two shape changes and did not carry them into the expectations: `fetchLiveCiAggregatePreferGraphQl` gained an 8th parameter, and the live-fact cache key gained an `|ign:` fragment. Two accept-path assertions additionally expected `undefined` for `advisoryCheckRuns` where the settings resolver supplies `null` -- the resolver returns the column's value rather than an absence, and `toHaveBeenCalledWith` distinguishes the two. Expectations only. The production code is correct, which is why nothing is broken at runtime and why this went unnoticed: #9813's own CI ran before the merges that made both call paths exercise it. --- test/unit/agent-approval-queue.test.ts | 6 +++--- test/unit/queue.test.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/unit/agent-approval-queue.test.ts b/test/unit/agent-approval-queue.test.ts index f38c30cf2..dfb0d8b74 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), null, null); }); 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), null, null); }); 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..64dc2f0f1 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 (likewise) ); expect(gateChecks).toBeGreaterThan(0); const finalized = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?") @@ -3168,8 +3169,8 @@ 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: the durable cache key now folds in the advisory-check-runs fingerprint (empty "|adv:|ign:" when unconfigured). + expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 7)).toMatchObject({ ciState: "passed", ciRequiredContextsKey: `${JSON.stringify(["trusted-required-ci"])}|adv:|ign:` }); } finally { liveCiSpy.mockRestore(); } From 26747428674cdaf90334c7fd0690ae1e79693ed7 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 29 Jul 2026 05:01:38 -0700 Subject: [PATCH 2/2] fix(config): document ignoredCheckRuns in the full example too, as its parity test requires (#9822) The second way #9813 left main red. `.loopover.yml.example` documents the new `gate.ignoredCheckRuns` field; `config/examples/loopover.full.yml` does not -- and `test/unit/config-templates.test.ts` asserts the two are byte-identical from the WHERE IT LIVES marker onward, precisely so a field cannot be documented in one place and missing from the other. The full example now carries the same block. Nothing else in the two bodies differed. --- config/examples/loopover.full.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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