@@ -6011,6 +6011,202 @@ describe("queue processors", () => {
60116011 expect(reuseAudit?.n).toBe(2); // both later passes explicitly reused the durable cache
60126012 });
60136013
6014+ it("REGRESSION (#6685): a draft PR's repeat fork-CI-completion triggers stop republishing once the head is already current", async () => {
6015+ const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
6016+ // reviewCheckMode: "disabled" reproduces the live incident exactly -- gateEnabled (shouldPublishReviewCheck
6017+ // && headSha) is false here, so the OLDER check-run-only skip guard (canSkipCurrentSurface, gated on
6018+ // gateEnabled) never engages; this fix must not depend on it.
6019+ await seedRegateChurnRepo(env, { publicSurface: "comment_and_label" });
6020+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_and_label", checkRunMode: "off", reviewCheckMode: "disabled", aiReviewMode: "block" }, review: { auto_review: { skip_drafts: true, cadence: "continuous" } } });
6021+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 90, title: "Draft feature", state: "open", draft: true, user: { login: "contributor" }, head: { sha: "a90" }, labels: [], body: "Closes #1" } as never);
6022+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 90, status: "complete", reviewsSyncedAt: new Date().toISOString() });
6023+
6024+ const stickyComment: { current: { id: number; body: string } | null } = { current: null };
6025+ let commentPosts = 0;
6026+ let commentPatches = 0;
6027+ let labelPosts = 0;
6028+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
6029+ const url = input.toString();
6030+ const method = init?.method ?? "GET";
6031+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
6032+ if (url.includes("/pulls/90/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]);
6033+ if (url.endsWith("/pulls/90")) return Response.json({ number: 90, title: "Draft feature", state: "open", draft: true, user: { login: "contributor" }, head: { sha: "a90" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
6034+ if (url.includes("/commits/a90/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
6035+ if (url.includes("/commits/a90/status")) return Response.json({ state: "success", statuses: [] });
6036+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
6037+ if (url.includes("/issues/90/comments") && method === "GET") {
6038+ return Response.json(stickyComment.current ? [{ ...stickyComment.current, user: { login: "gittensory[bot]", type: "Bot" } }] : []);
6039+ }
6040+ if (url.includes("/issues/90/comments") && method === "POST") {
6041+ commentPosts += 1;
6042+ const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "");
6043+ stickyComment.current = { id: 1, body };
6044+ return Response.json({ id: 1 }, { status: 201 });
6045+ }
6046+ if (url.includes("/issues/comments/1") && method === "PATCH") {
6047+ commentPatches += 1;
6048+ const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "");
6049+ stickyComment.current = { id: 1, body };
6050+ return Response.json({ id: 1 }, { status: 200 });
6051+ }
6052+ if (url.includes("/issues/90/labels") && method === "GET") return Response.json([]);
6053+ if (url.includes("/issues/90/labels") && method === "POST") {
6054+ labelPosts += 1;
6055+ return Response.json([]);
6056+ }
6057+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
6058+ return Response.json({});
6059+ });
6060+
6061+ // First pass: the initial publish for this head -- always goes through in full (a CREATE placeholder,
6062+ // then a PATCH to the final settled content, mirroring #3379's own first-pass shape).
6063+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-1", repoFullName: "JSONbored/gittensory", prNumber: 90, installationId: 123 });
6064+ expect(commentPosts).toBe(1);
6065+ const patchesAfterFirst = commentPatches;
6066+ const labelPostsAfterFirst = labelPosts;
6067+ expect(labelPostsAfterFirst).toBeGreaterThan(0);
6068+
6069+ // Three more fork-resume passes over the SAME unchanged head SHA (mirroring one webhook per completing
6070+ // CI job on a fork PR, confirmed live: 12 of these in 29 minutes for JSONbored/loopover#6592).
6071+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-2", repoFullName: "JSONbored/gittensory", prNumber: 90, installationId: 123 });
6072+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-3", repoFullName: "JSONbored/gittensory", prNumber: 90, installationId: 123 });
6073+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-4", repoFullName: "JSONbored/gittensory", prNumber: 90, installationId: 123 });
6074+
6075+ expect(commentPosts).toBe(1); // never re-created
6076+ expect(commentPatches).toBe(patchesAfterFirst); // never rewritten either — the whole publish pass is skipped, not just diffed away
6077+
6078+ const publishedAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?")
6079+ .bind("github_app.pr_public_surface_published", "JSONbored/gittensory#90")
6080+ .first<{ n: number }>();
6081+ expect(publishedAudit?.n).toBe(1); // only the first pass ever counts as a publish
6082+
6083+ const skippedAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?")
6084+ .bind("github_app.public_surface_publish_skipped_current", "JSONbored/gittensory#90")
6085+ .first<{ n: number }>();
6086+ expect(skippedAudit?.n).toBe(3); // all three repeat fork-resume passes were proven redundant up-front
6087+ });
6088+
6089+ it("REGRESSION (#6685): a draft PR's republish-skip does NOT apply once a new commit changes the head, or when a maintainer forces a re-trigger", async () => {
6090+ const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
6091+ await seedRegateChurnRepo(env, { publicSurface: "comment_and_label" });
6092+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_and_label", checkRunMode: "off", reviewCheckMode: "disabled", aiReviewMode: "block" }, review: { auto_review: { skip_drafts: true, cadence: "continuous" } } });
6093+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 91, title: "Draft feature", state: "open", draft: true, user: { login: "contributor" }, head: { sha: "b91" }, labels: [], body: "Closes #1" } as never);
6094+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 91, status: "complete", reviewsSyncedAt: new Date().toISOString() });
6095+
6096+ let headSha = "b91";
6097+ const stickyComment: { current: { id: number; body: string } | null } = { current: null };
6098+ let commentPosts = 0;
6099+ let commentPatches = 0;
6100+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
6101+ const url = input.toString();
6102+ const method = init?.method ?? "GET";
6103+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
6104+ if (url.includes(`/pulls/91/files`)) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]);
6105+ if (url.endsWith("/pulls/91")) return Response.json({ number: 91, title: "Draft feature", state: "open", draft: true, user: { login: "contributor" }, head: { sha: headSha }, labels: [], body: "Closes #1", mergeable_state: "clean" });
6106+ if (url.includes(`/commits/${headSha}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] });
6107+ if (url.includes(`/commits/${headSha}/status`)) return Response.json({ state: "success", statuses: [] });
6108+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
6109+ if (url.includes("/issues/91/comments") && method === "GET") {
6110+ return Response.json(stickyComment.current ? [{ ...stickyComment.current, user: { login: "gittensory[bot]", type: "Bot" } }] : []);
6111+ }
6112+ if (url.includes("/issues/91/comments") && method === "POST") {
6113+ commentPosts += 1;
6114+ const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "");
6115+ stickyComment.current = { id: 1, body };
6116+ return Response.json({ id: 1 }, { status: 201 });
6117+ }
6118+ if (url.includes("/issues/comments/1") && method === "PATCH") {
6119+ commentPatches += 1;
6120+ const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "");
6121+ stickyComment.current = { id: 1, body };
6122+ return Response.json({ id: 1 }, { status: 200 });
6123+ }
6124+ if (url.includes("/issues/91/labels") && method === "GET") return Response.json([]);
6125+ if (url.includes("/issues/91/labels") && method === "POST") return Response.json([]);
6126+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
6127+ return Response.json({});
6128+ });
6129+
6130+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-1", repoFullName: "JSONbored/gittensory", prNumber: 91, installationId: 123 });
6131+ expect(commentPosts).toBe(1);
6132+ const patchesAfterFirst = commentPatches;
6133+
6134+ // A new commit lands (new head SHA) -- the next fork-resume pass must NOT be treated as redundant. A
6135+ // repeat publish to an EXISTING sticky comment is a PATCH (update in place), not a second POST, so a
6136+ // real republish shows up as an additional PATCH here, not another create.
6137+ headSha = "c91";
6138+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 91, title: "Draft feature", state: "open", draft: true, user: { login: "contributor" }, head: { sha: headSha }, labels: [], body: "Closes #1" } as never);
6139+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-2", repoFullName: "JSONbored/gittensory", prNumber: 91, installationId: 123 });
6140+ const patchesAfterNewHead = commentPatches;
6141+ expect(patchesAfterNewHead).toBeGreaterThan(patchesAfterFirst); // republished (as an update) for the new head, not skipped
6142+
6143+ const publishedAfterNewHead = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?")
6144+ .bind("github_app.pr_public_surface_published", "JSONbored/gittensory#91")
6145+ .first<{ n: number }>();
6146+ expect(publishedAfterNewHead?.n).toBe(2); // the first publish, plus this one for the new head
6147+
6148+ // A maintainer explicitly forces a fresh pass over the SAME (now-current) head -- must not be skipped either.
6149+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-3", repoFullName: "JSONbored/gittensory", prNumber: 91, installationId: 123, force: true });
6150+ expect(commentPatches).toBeGreaterThan(patchesAfterNewHead);
6151+ const publishedAfterForce = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?")
6152+ .bind("github_app.pr_public_surface_published", "JSONbored/gittensory#91")
6153+ .first<{ n: number }>();
6154+ expect(publishedAfterForce?.n).toBe(3);
6155+ });
6156+
6157+ it("swallows a failing draft-republish-skip audit write without throwing (#6685)", async () => {
6158+ const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
6159+ await seedRegateChurnRepo(env, { publicSurface: "comment_and_label" });
6160+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_and_label", checkRunMode: "off", reviewCheckMode: "disabled", aiReviewMode: "block" }, review: { auto_review: { skip_drafts: true, cadence: "continuous" } } });
6161+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 92, title: "Draft feature", state: "open", draft: true, user: { login: "contributor" }, head: { sha: "d92" }, labels: [], body: "Closes #1" } as never);
6162+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 92, status: "complete", reviewsSyncedAt: new Date().toISOString() });
6163+
6164+ const stickyComment: { current: { id: number; body: string } | null } = { current: null };
6165+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
6166+ const url = input.toString();
6167+ const method = init?.method ?? "GET";
6168+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
6169+ if (url.includes("/pulls/92/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]);
6170+ if (url.endsWith("/pulls/92")) return Response.json({ number: 92, title: "Draft feature", state: "open", draft: true, user: { login: "contributor" }, head: { sha: "d92" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
6171+ if (url.includes("/commits/d92/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
6172+ if (url.includes("/commits/d92/status")) return Response.json({ state: "success", statuses: [] });
6173+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
6174+ if (url.includes("/issues/92/comments") && method === "GET") {
6175+ return Response.json(stickyComment.current ? [{ ...stickyComment.current, user: { login: "gittensory[bot]", type: "Bot" } }] : []);
6176+ }
6177+ if (url.includes("/issues/92/comments") && method === "POST") {
6178+ const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "");
6179+ stickyComment.current = { id: 1, body };
6180+ return Response.json({ id: 1 }, { status: 201 });
6181+ }
6182+ if (url.includes("/issues/comments/1") && method === "PATCH") {
6183+ const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "");
6184+ stickyComment.current = { id: 1, body };
6185+ return Response.json({ id: 1 }, { status: 200 });
6186+ }
6187+ if (url.includes("/issues/92/labels") && method === "GET") return Response.json([]);
6188+ if (url.includes("/issues/92/labels") && method === "POST") return Response.json([]);
6189+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
6190+ return Response.json({});
6191+ });
6192+
6193+ // First pass publishes for real (populating lastPublishedSurfaceSha).
6194+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-1", repoFullName: "JSONbored/gittensory", prNumber: 92, installationId: 123 });
6195+
6196+ const originalRecordAuditEvent = repositoriesModule.recordAuditEvent;
6197+ const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => {
6198+ if (event.eventType === "github_app.public_surface_publish_skipped_current") throw new Error("audit DB down");
6199+ await originalRecordAuditEvent(auditEnv, event);
6200+ });
6201+
6202+ // The second (repeat, same-head) pass takes the draft-skip path -- its audit write fails, but the job
6203+ // must still resolve cleanly rather than throwing.
6204+ await expect(
6205+ processJob(env, { type: "agent-regate-pr", deliveryId: "fork-resume-2", repoFullName: "JSONbored/gittensory", prNumber: 92, installationId: 123 }),
6206+ ).resolves.toBeUndefined();
6207+ auditSpy.mockRestore();
6208+ });
6209+
60146210 it("a PURE base-branch movement (no reviewed content change) triggers neither a fresh AI review nor a comment rewrite", async () => {
60156211 let aiCalls = 0;
60166212 const env = createTestEnv({
0 commit comments