diff --git a/apps/gittensory-ui/src/components/site/proof-of-power-stats.test.tsx b/apps/gittensory-ui/src/components/site/proof-of-power-stats.test.tsx index e1b81a9bdb..00f93671bf 100644 --- a/apps/gittensory-ui/src/components/site/proof-of-power-stats.test.tsx +++ b/apps/gittensory-ui/src/components/site/proof-of-power-stats.test.tsx @@ -1,12 +1,14 @@ import { render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import type { ReactNode } from "react"; const { apiFetch } = vi.hoisted(() => ({ apiFetch: vi.fn() })); vi.mock("@/lib/api/request", () => ({ apiFetch: (...args: unknown[]) => apiFetch(...args) })); vi.mock("@/lib/api/origin", () => ({ getApiOrigin: () => "https://api.test" })); +afterEach(() => vi.unstubAllGlobals()); + import { ProofOfPowerStats } from "@/components/site/proof-of-power-stats"; import { formatStatsAgo, @@ -121,8 +123,18 @@ describe("ProofOfPowerStats", () => { }); it("settles the count-up on the real reviewed total (not stuck at 0 when rAF never fires)", async () => { + // Deterministic (#flake): force prefers-reduced-motion so useCountUp lands the final value synchronously on + // mount, instead of running the requestAnimationFrame tween. jsdom has no matchMedia, so the unfixed test took + // the animated path and raced the 3s findByText timeout under CI load. This still pins the intent — the count + // settles on the real reviewed total, never stuck at 0 — without depending on animation-frame timing. + vi.stubGlobal("matchMedia", () => ({ + matches: true, + media: "(prefers-reduced-motion: reduce)", + addEventListener: () => {}, + removeEventListener: () => {}, + })); apiFetch.mockResolvedValue({ ok: true, status: 200, durationMs: 1, data: PAYLOAD }); renderWithClient(); - expect(await screen.findByText("2,708", undefined, { timeout: 3000 })).toBeTruthy(); + expect(await screen.findByText("2,708")).toBeTruthy(); }); }); diff --git a/migrations/0080_pr_last_published_surface_sha.sql b/migrations/0080_pr_last_published_surface_sha.sql new file mode 100644 index 0000000000..c800a57069 --- /dev/null +++ b/migrations/0080_pr_last_published_surface_sha.sql @@ -0,0 +1,7 @@ +-- Over-publish dedup (#4): the head SHA at which a PR's public surface (comment/label/check-run) was LAST +-- published. The scheduled re-gate sweep skips re-reviewing + re-publishing a PR while +-- last_published_surface_sha === head_sha (the surface is already current). Keyed to the head SHA so a push / +-- rebase / force-push (new head) no longer matches → the next sweep re-reviews + re-publishes the new code. +-- NULL = never published. gittensory-computed (publish-written); like approved_head_sha / merge_blocked_sha it is +-- omitted from the GitHub-sync SET clause so a later sync cannot clobber it. +ALTER TABLE pull_requests ADD COLUMN last_published_surface_sha TEXT; diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 4bcc77b060..41482147af 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -2627,6 +2627,20 @@ export async function markPullRequestApproved(env: Env, fullName: string, number .where(and(eq(pullRequests.repoFullName, fullName), eq(pullRequests.number, number), eq(pullRequests.headSha, headSha))); } +/** Over-publish dedup (#4): record the head SHA at which the PR's public surface was just published. The scheduled + * re-gate sweep skips re-reviewing while last_published_surface_sha == headSha. Scoped to headSha so a later commit + * (push/rebase/force-push — the live head no longer matches) re-publishes the new code without any manual reset. + * The eq(headSha) in the WHERE is load-bearing: if the live head advanced between review and this write, the UPDATE + * no-ops (never stamps a stale head) → the next sweep correctly re-reviews. Mirrors markPullRequestApproved. */ +export async function markPullRequestSurfacePublished(env: Env, fullName: string, number: number, headSha: string | null | undefined): Promise { + if (!headSha) return; // no head to key the marker on → nothing to stamp (the caller's advisory had no head SHA) + const db = getDb(env.DB); + await db + .update(pullRequests) + .set({ lastPublishedSurfaceSha: headSha, updatedAt: nowIso() }) + .where(and(eq(pullRequests.repoFullName, fullName), eq(pullRequests.number, number), eq(pullRequests.headSha, headSha))); +} + /** Sweep convergence: stamp the timestamp the scheduled re-gate sweep just recomputed this PR. A plain D1 UPDATE * — NOT routed through the agent-action-executor chokepoint (#1258) — so it advances even when GitHub writes are * suppressed (dry-run / paused). selectRegateCandidates orders the sweep by last_regated_at, so a just-regated PR @@ -4161,6 +4175,7 @@ function toPullRequestRecordFromRow(row: typeof pullRequests.$inferSelect): Pull approvedHeadSha: row.approvedHeadSha, // Read straight from the row, NEVER the GitHub payload — this is a gittensory-internal sweep marker. lastRegatedAt: row.lastRegatedAt, + lastPublishedSurfaceSha: row.lastPublishedSurfaceSha, }; } diff --git a/src/db/schema.ts b/src/db/schema.ts index 5c60600d8d..396992ab51 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -304,6 +304,12 @@ export const pullRequests = sqliteTable( // review WRITE that would bump updated_at is suppressed (dry-run / paused). gittensory-computed (sweep-written), // omitted from the GitHub-sync SET clause so a later sync cannot clobber it. (Mirrors approved_head_sha.) lastRegatedAt: text("last_regated_at"), + // Over-publish dedup: the head SHA at which the public surface (comment/label/check-run) was LAST published. + // The sweep skips re-reviewing + re-publishing a PR while last_published_surface_sha === headSha (already + // current). Keyed to head SHA → a push/rebase/force-push (new head) clears the match and the next sweep + // re-reviews + re-publishes. gittensory-computed (publish-written), omitted from the GitHub-sync SET clause so + // a later sync cannot clobber it. (Mirrors approved_head_sha.) + lastPublishedSurfaceSha: text("last_published_surface_sha"), createdAt: text("created_at").notNull().$defaultFn(() => nowIso()), updatedAt: text("updated_at").notNull().$defaultFn(() => nowIso()), }, diff --git a/src/github/comments.ts b/src/github/comments.ts index 368caabc8d..f31a2d7bb2 100644 --- a/src/github/comments.ts +++ b/src/github/comments.ts @@ -72,6 +72,11 @@ async function createOrUpdateIssueCommentWithMarker( if (batch.length < 100) break; } if (existing) { + // Idempotency (#4): skip the PATCH when the rendered body is byte-identical to what's already posted. The + // re-gate sweep re-renders the same surface every cycle for an unchanged PR; without this, every cycle PATCHes + // GitHub (a write + rate-limit cost) for no visible change. Defense-in-depth alongside the head_sha publish + // marker — also collapses a duplicate webhook delivery for the same commit. + if (existing.body === body) return { id: existing.id, ...(existing.html_url !== undefined ? { html_url: existing.html_url } : {}) }; const response = await octokit.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { owner, repo, diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 0edd9a835c..022f413516 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -41,6 +41,7 @@ import { getCachedAiReview, putCachedAiReview, markPullRequestsRegated, + markPullRequestSurfacePublished, getLatestRegatedAt, claimRegateFanoutSlot, recordAgentCommandFeedback, @@ -1508,6 +1509,16 @@ async function reReviewStoredPullRequest( /* v8 ignore next -- the row was just upserted above, so the re-read always returns it; `?? pr` is belt-and-suspenders fail-open. */ pr = (await getPullRequest(env, repoFullName, prNumber)) ?? pr; } + // Over-publish dedup (#4): the resync above made pr.headSha the LIVE head. If the public surface was already + // published at this exact head, the verdict + comment are already current — skip the re-review + re-publish so the + // sweep stops re-publishing every open PR every ~2-min cycle. A never-published PR (NULL marker) or a drifted head + // (push/rebase/force-push → marker !== live head) falls THROUGH and re-reviews at the new head; the AI cache is + // head_sha-keyed too, so a rebase misses it and gets a fresh review. (Webhook synchronize/opened paths review + // directly and always re-stamp — this guard only gates the scheduled sweep.) + if (pr.lastPublishedSurfaceSha && pr.lastPublishedSurfaceSha === pr.headSha) { + console.log(JSON.stringify({ level: "info", event: "rereview_skipped_surface_current", deliveryId, repository: repoFullName, pullNumber: prNumber, headSha: pr.headSha })); + return; + } // Operator review flow: rebase-if-behind → wait for ALL CI to finish → only THEN review. Defers (returns) when // a rebase fired a synchronize, or CI is still running — the synchronize / CI-completion webhook re-triggers // once the head is current and CI has settled (the sweep backstops a missed event). REST-budget dedup @@ -5328,6 +5339,13 @@ async function maybePublishPrPublicSurface( failedOutputs, }, }); + // Over-publish dedup (#4): stamp the head SHA we just published at, so the scheduled sweep skips re-reviewing + + // re-publishing this PR until its head changes (see the guard in reReviewStoredPullRequest). Reached only when at + // least one surface output actually published (the zero-output early-return above covers the suppressed/dry-run + // case). The helper no-ops on a null head, and its WHERE pins head_sha so a head that advanced mid-pass won't stamp. + await markPullRequestSurfacePublished(env, repoFullName, pr.number, advisory.headSha).catch((error) => { + console.error(JSON.stringify({ level: "warn", event: "surface_published_mark_failed", repoFullName, pullNumber: pr.number, error: errorMessage(error) })); + }); return gateEvaluation; } diff --git a/src/types.ts b/src/types.ts index fd7e2379d6..eea8fab367 100644 --- a/src/types.ts +++ b/src/types.ts @@ -453,6 +453,10 @@ export type PullRequestRecord = { * review write that would bump updatedAt is suppressed (dry-run / paused). Sweep-written; read straight from * the row (never the GitHub payload). */ lastRegatedAt?: string | null | undefined; + /** Over-publish dedup: the head SHA at which the public surface was last published. The re-gate sweep skips + * re-reviewing + re-publishing while lastPublishedSurfaceSha === headSha; a new commit (push/rebase/force-push) + * clears the match so the surface re-publishes the new code. Publish-written; read straight from the row. */ + lastPublishedSurfaceSha?: string | null | undefined; }; export type IssueRecord = { diff --git a/test/unit/db-parsers.test.ts b/test/unit/db-parsers.test.ts index 300b326ec0..e55ceb8c96 100644 --- a/test/unit/db-parsers.test.ts +++ b/test/unit/db-parsers.test.ts @@ -12,6 +12,7 @@ import { listRepoSyncStates, markPullRequestRegated, markPullRequestsRegated, + markPullRequestSurfacePublished, recordAuditEvent, recordWebhookEvent, upsertOfficialMinerDetection, @@ -101,6 +102,25 @@ describe("database row parser hardening", () => { expect(after?.title).toBe("Stale PR"); // INVARIANT: a plain D1 UPDATE — it touches only the marker, not PR content }); + it("markPullRequestSurfacePublished stamps last_published_surface_sha only at the matching live head (#4 over-publish dedup)", async () => { + const env = createTestEnv(); + await upsertPullRequestFromGitHub(env, "owner/repo", { number: 9, title: "PR", state: "open", user: { login: "alice" }, head: { sha: "headA" }, labels: [] }); + + const before = (await listPullRequests(env, "owner/repo")).find((p) => p.number === 9); + expect(before?.lastPublishedSurfaceSha ?? null).toBeNull(); // never published → marker absent + + await markPullRequestSurfacePublished(env, "owner/repo", 9, null); // null head → no-op (the !headSha guard) + expect((await listPullRequests(env, "owner/repo")).find((p) => p.number === 9)?.lastPublishedSurfaceSha ?? null).toBeNull(); + + await markPullRequestSurfacePublished(env, "owner/repo", 9, "oldHead"); // stale head → WHERE head_sha mismatch → no-op + expect((await listPullRequests(env, "owner/repo")).find((p) => p.number === 9)?.lastPublishedSurfaceSha ?? null).toBeNull(); + + await markPullRequestSurfacePublished(env, "owner/repo", 9, "headA"); // matches the live head → stamps + const after = (await listPullRequests(env, "owner/repo")).find((p) => p.number === 9); + expect(after?.lastPublishedSurfaceSha).toBe("headA"); + expect(after?.title).toBe("PR"); // INVARIANT: touches only the marker, not PR content + }); + it("markPullRequestsRegated batch-stamps every candidate at dispatch and no-ops on an empty list (#audit-sweep-dispatch-stamp)", async () => { const env = createTestEnv(); for (const number of [5, 6, 7]) { diff --git a/test/unit/github-comments.test.ts b/test/unit/github-comments.test.ts index 7a855ed6b4..483e2b7760 100644 --- a/test/unit/github-comments.test.ts +++ b/test/unit/github-comments.test.ts @@ -229,6 +229,46 @@ describe("GitHub PR intelligence comments", () => { expect(calls.some((call) => call.startsWith("POST ") && call.includes("/issues/12/comments"))).toBe(true); }); + it("skips the PATCH when the existing sticky comment body is byte-identical (#4 idempotency), keeping html_url", async () => { + const privateKey = await generatePrivateKeyPem(); + const body = `${PR_INTELLIGENCE_COMMENT_MARKER}\nidentical body`; + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/12/comments") && (init?.method ?? "GET") === "GET") { + return Response.json([{ id: 101, body, html_url: "https://github.com/comment/101", user: { login: "gittensory[bot]", type: "Bot" } }]); + } + return new Response("not found", { status: 404 }); + }); + + const result = await createOrUpdatePrIntelligenceComment(createTestEnv({ GITHUB_APP_PRIVATE_KEY: privateKey }), 123, "JSONbored/gittensory", 12, body); + + expect(result).toEqual({ id: 101, html_url: "https://github.com/comment/101" }); // html_url-present branch of the early return + expect(calls.some((call) => call.startsWith("PATCH "))).toBe(false); // identical body → NO GitHub write + }); + + it("skips the PATCH on an identical body even when the existing comment has no html_url (#4 idempotency)", async () => { + const privateKey = await generatePrivateKeyPem(); + const body = `${PR_INTELLIGENCE_COMMENT_MARKER}\nidentical body`; + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/12/comments") && (init?.method ?? "GET") === "GET") { + return Response.json([{ id: 202, body, user: { login: "gittensory[bot]", type: "Bot" } }]); // no html_url field + } + return new Response("not found", { status: 404 }); + }); + + const result = await createOrUpdatePrIntelligenceComment(createTestEnv({ GITHUB_APP_PRIVATE_KEY: privateKey }), 123, "JSONbored/gittensory", 12, body); + + expect(result).toEqual({ id: 202 }); // html_url-absent branch → no html_url key on the early return + expect(calls.some((call) => call.startsWith("PATCH "))).toBe(false); + }); + it("rejects invalid repository names before calling GitHub", async () => { await expect(createOrUpdatePrIntelligenceComment(createTestEnv(), 123, "invalid", 12, "body")).rejects.toThrow(/Invalid repository full name/); }); diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 104e576e93..74123a9a72 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -864,6 +864,102 @@ describe("queue processors", () => { resyncUpsertSpy.mockRestore(); }); + it("#4 over-publish dedup: the sweep SKIPS re-review when the surface was already published at the current head", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Current PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); // already published at the live head + let checkRunsFetched = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Current PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); // live head matches → no drift + if (url.includes("/check-runs")) { checkRunsFetched = true; return Response.json({ total_count: 0, check_runs: [] }); } + if (url.includes("/status")) return Response.json({ state: "success", statuses: [] }); + return Response.json({}); + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "skip-current", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); + + // The dedup guard returned BEFORE prReadyForReview → no CI check-runs were fetched (the re-review never ran). + expect(checkRunsFetched).toBe(false); + }); + + it("#4 over-publish dedup: a rebased PR (marker != live head) is NOT skipped — it resyncs + re-reviews at the new head, and the marker survives the resync", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Rebased PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); // published at the OLD head a7 + let checkRunsFetchedAtNewHead = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Rebased PR", state: "open", user: { login: "contributor" }, head: { sha: "b8" }, labels: [], body: "Closes #1" }); // LIVE head drifted to b8 (rebase/force-push) + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.includes("/commits/b8/check-runs")) { checkRunsFetchedAtNewHead = true; return Response.json({ total_count: 0, check_runs: [] }); } + if (url.includes("/commits/b8/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("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "rebase-rereview", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); + + // The old-head marker (a7) != the live rebased head (b8) → the guard fell THROUGH → the PR was resynced to b8 and + // re-reviewed at the new head (check-runs fetched at b8). The marker is NOT in the GitHub-sync SET clause, so the + // resync upsert preserved it (still a7) until a fresh publish advances it — proving rebases are never skipped. + expect(checkRunsFetchedAtNewHead).toBe(true); + const stored = await getPullRequest(env, "owner/agent-repo", 7); + expect(stored?.headSha).toBe("b8"); + expect(stored?.lastPublishedSurfaceSha).toBe("a7"); // marker survived the resync (omitted from the sync SET clause) + }); + + it("#4 over-publish dedup: a failing surface-published stamp is swallowed (fail-open) — the publish still completes", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", aiReviewMode: "off", gatePack: "oss-anti-slop" }); + const stampSpy = vi.spyOn(repositoriesModule, "markPullRequestSurfacePublished").mockRejectedValueOnce(new Error("D1 stamp failed")); + let commentPosted = false; + 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: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/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/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") { commentPosted = true; return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "stamp-failopen", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, + }, + }), + ).resolves.toBeUndefined(); + + expect(commentPosted).toBe(true); // the surface published despite the marker write throwing + expect(stampSpy).toHaveBeenCalled(); + stampSpy.mockRestore(); + }); + it("#1: the block-mode re-gate sweep replays cached AI findings before gate evaluation", async () => { let aiCalls = 0; const env = createTestEnv({