From d9d55090de69283fa7d71b657425b1f9969a7347 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:40:23 -0700 Subject: [PATCH 1/2] fix(review): replay linked issue one-shot blockers --- src/db/repositories.ts | 28 +++++++++++++ src/queue/processors.ts | 15 +++++++ .../linked-issue-satisfaction-cache.test.ts | 37 ++++++++++++++++- test/unit/queue.test.ts | 41 +++++++++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 80e687b920..45c82bcf01 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -4794,6 +4794,34 @@ export async function hasPublishedLinkedIssueSatisfaction( return Boolean(row); } +/** #one-shot-review-cadence: latest stored linked-issue satisfaction assessment for this PR + primary issue, + * regardless of head SHA or fingerprint. One-shot repeat triggers intentionally freeze the first-pass AI + * result rather than re-spending, but `block` mode still needs the prior unaddressed verdict replayed so the + * configured gate blocker cannot disappear on the next automatic evaluation. */ +export async function getLatestPublishedLinkedIssueSatisfaction( + env: Env, + repoFullName: string, + pullNumber: number, + linkedIssueNumber: number, +): Promise<{ status: string; result: LinkedIssueSatisfactionResult | null; estimatedNeurons: number } | null> { + const row = await env.DB + .prepare( + `SELECT status, result_json AS resultJson, estimated_neurons AS estimatedNeurons + FROM linked_issue_satisfaction_cache + WHERE repo_full_name = ? AND pull_number = ? AND linked_issue_number = ? + ORDER BY created_at DESC, head_sha DESC + LIMIT 1`, + ) + .bind(repoFullName, pullNumber, linkedIssueNumber) + .first<{ status: string; resultJson: string | null; estimatedNeurons: number }>(); + if (!row) return null; + return { + status: row.status, + result: parseJson(row.resultJson, null), + estimatedNeurons: row.estimatedNeurons, + }; +} + /** #4499 (grounding-file-content-cache): the stored file content for (repo, path, head SHA), or null on a * miss. Unlike linked_issue_satisfaction_cache, every stored row is durable with NO input-fingerprint * dimension -- file content at an immutable head SHA has exactly one correct value, so a hit is always safe diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 8e25c88d93..a066963978 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -54,6 +54,7 @@ import { putCachedAiSlopAdvisory, hasPublishedAiSlopAdvisory, getCachedLinkedIssueSatisfaction, + getLatestPublishedLinkedIssueSatisfaction, putCachedLinkedIssueSatisfaction, hasPublishedLinkedIssueSatisfaction, markPullRequestsRegated, @@ -10177,6 +10178,20 @@ async function maybePublishPrPublicSurface( primaryLinkedIssueNumber !== undefined && (await hasPublishedLinkedIssueSatisfaction(env, repoFullName, pr.number, primaryLinkedIssueNumber).catch(() => false)); if (linkedIssueOneShotSkip) { + const priorLinkedIssueSatisfaction = await getLatestPublishedLinkedIssueSatisfaction(env, repoFullName, pr.number, primaryLinkedIssueNumber).catch(() => null); + if (priorLinkedIssueSatisfaction?.status === "ok" && priorLinkedIssueSatisfaction.result) { + linkedIssueSatisfaction = { status: priorLinkedIssueSatisfaction.result.status, rationale: priorLinkedIssueSatisfaction.result.rationale }; + if (settings.linkedIssueSatisfactionGateMode === "block" && priorLinkedIssueSatisfaction.result.status === "unaddressed") { + advisory.findings.push({ + code: "linked_issue_scope_mismatch", + severity: "warning", + title: "Linked issue does not appear to be satisfied", + detail: priorLinkedIssueSatisfaction.result.rationale, + action: "Confirm this PR actually addresses the linked issue's scope, or link the correct issue.", + publicText: `AI assessment: this PR does not appear to satisfy its linked issue's scope. ${priorLinkedIssueSatisfaction.result.rationale}`, + }); + } + } await recordAuditEvent(env, { eventType: "github_app.linked_issue_satisfaction_one_shot_skip", actor: author, diff --git a/test/unit/linked-issue-satisfaction-cache.test.ts b/test/unit/linked-issue-satisfaction-cache.test.ts index 1bd89cf155..1d94133c61 100644 --- a/test/unit/linked-issue-satisfaction-cache.test.ts +++ b/test/unit/linked-issue-satisfaction-cache.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { getCachedLinkedIssueSatisfaction, hasPublishedLinkedIssueSatisfaction, putCachedLinkedIssueSatisfaction } from "../../src/db/repositories"; +import { getCachedLinkedIssueSatisfaction, getLatestPublishedLinkedIssueSatisfaction, hasPublishedLinkedIssueSatisfaction, putCachedLinkedIssueSatisfaction } from "../../src/db/repositories"; import { linkedIssueSatisfactionCacheInputFingerprint } from "../../src/review/linked-issue-satisfaction-cache-input"; import { createTestEnv } from "../helpers/d1"; @@ -114,6 +114,41 @@ describe("hasPublishedLinkedIssueSatisfaction (#one-shot-review-cadence)", () => }); }); +describe("getLatestPublishedLinkedIssueSatisfaction (#one-shot-review-cadence)", () => { + it("returns null when no row exists for the PR + linked issue number", async () => { + const env = createTestEnv(); + expect(await getLatestPublishedLinkedIssueSatisfaction(env, "o/r", 30, 1)).toBeNull(); + }); + + it("returns the latest row for the same PR + linked issue number regardless of head SHA or fingerprint", async () => { + const env = createTestEnv(); + vi.useFakeTimers(); + try { + vi.setSystemTime(new Date("2026-07-07T09:00:00.000Z")); + await putCachedLinkedIssueSatisfaction(env, "o/r", 31, "sha1", 5, "old-fp", { + status: "ok", + result: { status: "addressed", rationale: "old pass", confidence: 0.9 }, + estimatedNeurons: 4, + }); + vi.setSystemTime(new Date("2026-07-07T09:01:00.000Z")); + await putCachedLinkedIssueSatisfaction(env, "o/r", 31, "sha2", 5, "new-fp", { + status: "ok", + result: { status: "unaddressed", rationale: "latest blocker", confidence: 0.9 }, + estimatedNeurons: 8, + }); + } finally { + vi.useRealTimers(); + } + + expect(await getLatestPublishedLinkedIssueSatisfaction(env, "o/r", 31, 5)).toEqual({ + status: "ok", + result: { status: "unaddressed", rationale: "latest blocker", confidence: 0.9 }, + estimatedNeurons: 8, + }); + expect(await getLatestPublishedLinkedIssueSatisfaction(env, "o/r", 31, 6)).toBeNull(); + }); +}); + describe("linkedIssueSatisfactionCacheInputFingerprint", () => { it("is stable for the same input", async () => { const a = await linkedIssueSatisfactionCacheInputFingerprint({ byok: false, provider: null, model: null }); diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index f5654cae01..24250727e1 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -6094,6 +6094,47 @@ describe("queue processors", () => { expect(skipAudit?.detail).toContain("one-shot review cadence"); }); + it("default (one_shot): a repeat trigger replays a cached unaddressed linked-issue blocker in block mode", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "fresh call should not run" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "block" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 98, title: "Same-issue blocker PR", state: "open", user: { login: "contributor" }, head: { sha: "a98-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 98, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 98, "a98-v1", 1, "seed-fp", { status: "ok", result: { status: "unaddressed", rationale: "still does not implement the requested stream", confidence: 0.91 }, estimatedNeurons: 4 }); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/98/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/98")) return Response.json({ number: 98, title: "Same-issue blocker PR", state: "open", user: { login: "contributor" }, head: { sha: "a98-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a98-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a98-v2/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/98/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-same-issue-blocker-push", repoFullName: "JSONbored/gittensory", prNumber: 98, installationId: 123 }); + + expect(aiCalls).toBe(0); + const summary = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 98, "a98-v2") + .first<{ conclusion: string }>(); + expect(summary?.conclusion).toBe("failure"); + const blocker = await env.DB.prepare("select blocker_codes_json as blockerCodesJson from gate_outcomes where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 98, "a98-v2") + .first<{ blockerCodesJson: string }>(); + expect(JSON.parse(blocker?.blockerCodesJson ?? "[]")).toContain("linked_issue_scope_mismatch"); + }); + it("per-repo override (continuous via .gittensory.yml): a new push DOES spend a fresh main-review AI call, unlike the one_shot default", async () => { let aiCalls = 0; const env = createTestEnv({ From c3f981377c9902f7cdcfbc54942cc50dfea7d710 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:10:09 -0700 Subject: [PATCH 2/2] test(review): cover the 3 remaining linked-issue replay branches Closes the codecov/patch gap: a failed replay read (fail-safe fall through), an "addressed" prior verdict in block mode (no blocker pushed), and an unaddressed prior verdict outside block mode (reused for display, no hard blocker). --- test/unit/queue.test.ts | 137 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 24250727e1..47f2256178 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -6135,6 +6135,143 @@ describe("queue processors", () => { expect(JSON.parse(blocker?.blockerCodesJson ?? "[]")).toContain("linked_issue_scope_mismatch"); }); + it("default (one_shot, block mode): a failed replay-lookup falls through without pushing a blocker (fail-safe), even though a genuinely unaddressed row exists", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "fresh call should not run" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "block" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 101, title: "Replay-read-fails PR", state: "open", user: { login: "contributor" }, head: { sha: "a101-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 101, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + // The EXISTENCE check (hasPublishedLinkedIssueSatisfaction) that gates the one-shot skip stays real and + // sees this row -- only the separate REPLAY read (getLatestPublishedLinkedIssueSatisfaction) below is + // made to fail, proving its `.catch(() => null)` fail-safe never spuriously blocks (or throws) a PR just + // because the prior verdict couldn't be read back for replay. + await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 101, "a101-v1", 1, "seed-fp", { status: "ok", result: { status: "unaddressed", rationale: "still does not implement the requested stream", confidence: 0.91 }, estimatedNeurons: 4 }); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/101/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/101")) return Response.json({ number: 101, title: "Replay-read-fails PR", state: "open", user: { login: "contributor" }, head: { sha: "a101-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a101-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a101-v2/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/101/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + const readSpy = vi.spyOn(repositoriesModule, "getLatestPublishedLinkedIssueSatisfaction").mockRejectedValueOnce(new Error("D1 read error")); + await expect( + processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-replay-read-fails", repoFullName: "JSONbored/gittensory", prNumber: 101, installationId: 123 }), + ).resolves.toBeUndefined(); + readSpy.mockRestore(); + + expect(aiCalls).toBe(0); // still skipped -- the existence check alone gates the one-shot skip + const summary = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 101, "a101-v2") + .first<{ conclusion: string }>(); + expect(summary?.conclusion).not.toBe("failure"); + const blocker = await env.DB.prepare("select blocker_codes_json as blockerCodesJson from gate_outcomes where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 101, "a101-v2") + .first<{ blockerCodesJson: string }>(); + expect(JSON.parse(blocker?.blockerCodesJson ?? "[]")).not.toContain("linked_issue_scope_mismatch"); + const skipAudit = await env.DB.prepare("select outcome from audit_events where event_type = ? and target_key = ?") + .bind("github_app.linked_issue_satisfaction_one_shot_skip", "JSONbored/gittensory#101") + .first<{ outcome: string }>(); + expect(skipAudit?.outcome).toBe("completed"); // the skip itself still completes normally despite the failed replay read + }); + + it("default (one_shot, block mode): a replayed 'addressed' prior verdict never pushes a blocker (only an 'unaddressed' verdict does)", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "unaddressed", rationale: "fresh call should not run" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "block" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 102, title: "Addressed-replay PR", state: "open", user: { login: "contributor" }, head: { sha: "a102-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 102, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 102, "a102-v1", 1, "seed-fp", { status: "ok", result: { status: "addressed", rationale: "fully implements the requested stream", confidence: 0.95 }, estimatedNeurons: 4 }); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/102/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/102")) return Response.json({ number: 102, title: "Addressed-replay PR", state: "open", user: { login: "contributor" }, head: { sha: "a102-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a102-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a102-v2/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/102/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-addressed-replay", repoFullName: "JSONbored/gittensory", prNumber: 102, installationId: 123 }); + + expect(aiCalls).toBe(0); + const summary = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 102, "a102-v2") + .first<{ conclusion: string }>(); + expect(summary?.conclusion).not.toBe("failure"); + const blocker = await env.DB.prepare("select blocker_codes_json as blockerCodesJson from gate_outcomes where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 102, "a102-v2") + .first<{ blockerCodesJson: string }>(); + expect(JSON.parse(blocker?.blockerCodesJson ?? "[]")).not.toContain("linked_issue_scope_mismatch"); + }); + + it("default (one_shot, advisory mode): a replayed unaddressed prior verdict is reused for display but never pushes a hard blocker (only block mode does)", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "fresh call should not run" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + // advisory (not block) -- the same "unaddressed" stored verdict as the block-mode regression test above, + // so the ONLY variable changed is linkedIssueSatisfactionGateMode itself. + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "advisory" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 103, title: "Advisory-replay PR", state: "open", user: { login: "contributor" }, head: { sha: "a103-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 103, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 103, "a103-v1", 1, "seed-fp", { status: "ok", result: { status: "unaddressed", rationale: "still does not implement the requested stream", confidence: 0.91 }, estimatedNeurons: 4 }); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/103/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/103")) return Response.json({ number: 103, title: "Advisory-replay PR", state: "open", user: { login: "contributor" }, head: { sha: "a103-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a103-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a103-v2/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/103/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-advisory-replay", repoFullName: "JSONbored/gittensory", prNumber: 103, installationId: 123 }); + + expect(aiCalls).toBe(0); + const summary = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 103, "a103-v2") + .first<{ conclusion: string }>(); + expect(summary?.conclusion).not.toBe("failure"); // advisory mode never hard-blocks, even on an unaddressed replay + const blocker = await env.DB.prepare("select blocker_codes_json as blockerCodesJson from gate_outcomes where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 103, "a103-v2") + .first<{ blockerCodesJson: string }>(); + expect(JSON.parse(blocker?.blockerCodesJson ?? "[]")).not.toContain("linked_issue_scope_mismatch"); + }); + it("per-repo override (continuous via .gittensory.yml): a new push DOES spend a fresh main-review AI call, unlike the one_shot default", async () => { let aiCalls = 0; const env = createTestEnv({