From f5891725ce0edc398e5dc614fc07afeaba4be606 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 03:05:39 +0800 Subject: [PATCH 01/12] fix(agent-actions): apply account-age throttle on issue contributor-cap path Wire accountAgeThresholdDays into maybeCloseIssueOverContributorCap and label newly opened issues from below-threshold accounts, completing the Co-authored-by: Cursor #2561 issue-path gap documented in RepositorySettings. --- src/queue/processors.ts | 57 ++++++++++++++++++++++++- src/types.ts | 9 ++-- test/unit/queue.test.ts | 95 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 7 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 31b8ea45ac..7f1b862cba 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -4823,6 +4823,18 @@ async function maybeCloseIssueOverContributorCap( const authorIsAutomationBot = isProtectedAutomationAuthor(authorLogin); if (authorIsOwner || authorIsAdmin || authorIsAutomationBot) return; + // Account-age throttle (#2561): mirror the PR-path cap tightening — a below-threshold author gets half + // the configured per-repo issue cap (rounded up, minimum 1). Fail-open when created_at cannot be resolved. + let isNewAccount = false; + const accountAgeThresholdDays = settings.accountAgeThresholdDays; + if (typeof accountAgeThresholdDays === "number") { + const createdAt = await getGithubUserCreatedAt(env, installationId, authorLogin); + if (createdAt) { + const ageDays = (Date.now() - Date.parse(createdAt)) / (24 * 60 * 60 * 1000); + isNewAccount = ageDays < accountAgeThresholdDays; + } + } + // Install-wide check first (#2562): reuses the shared autoCloseExemptLogins list, same as the PR path. // verifiedGlobalOpenItemCount live-verifies every OTHER counted item before trusting it toward an // irreversible close (#2562 gate-review follow-up), mirroring the per-repo cap's own sibling live-verify. @@ -4873,6 +4885,9 @@ async function maybeCloseIssueOverContributorCap( // cooldown already honor -- see the matching comment on the PR-side per-repo cap in the PR maintenance path. if (typeof cap !== "number" || isAutoCloseExempt(authorLogin, settings.autoCloseExemptLogins)) return; + const effectiveIssueCap = + isNewAccount ? Math.max(1, Math.ceil(cap / 2)) : cap; + const otherOpenIssues = await listOpenIssues(env, repoFullName); const authorLoginLower = authorLogin.toLowerCase(); const otherAuthorIssueNumbers = otherOpenIssues @@ -4910,7 +4925,7 @@ async function maybeCloseIssueOverContributorCap( .filter((number) => confirmedOpen.has(number)) .concat(issue.number) .sort((a, b) => a - b); - const overCapNumbers = new Set(authorOpenIssueNumbers.slice(cap)); + const overCapNumbers = new Set(authorOpenIssueNumbers.slice(effectiveIssueCap)); if (overCapNumbers.size === 0) return; const planned = planAgentMaintenanceActions({ @@ -4923,7 +4938,7 @@ async function maybeCloseIssueOverContributorCap( authorIsAdmin, authorIsAutomationBot, ciState: "unverified", - contributorCapMatch: { matched: true, authorLogin, openCount: authorOpenIssueNumbers.length, cap, itemKind: "issues" }, + contributorCapMatch: { matched: true, authorLogin, openCount: authorOpenIssueNumbers.length, cap: effectiveIssueCap, itemKind: "issues" }, contributorCapLabel: settings.contributorCapLabel, pr: { labels: [] }, }); @@ -5615,6 +5630,44 @@ async function processGitHubWebhook( ); } await persistAdvisory(env, advisory); + // Account-age visibility (#2561 issue-path parity): label newly opened issues from below-threshold + // accounts when review_state_label autonomy is auto — same contract as the PR maintenance path. + if (payload.action === "opened" && installationId && issue.authorLogin) { + const repoOwner = payload.repository.full_name.includes("/") + ? payload.repository.full_name.slice(0, payload.repository.full_name.indexOf("/")) + : ""; + const authorLogin = issue.authorLogin; + const authorIsOwner = authorLogin.toLowerCase() === repoOwner.toLowerCase(); + const authorIsAdmin = parseGitHubLoginList(env.ADMIN_GITHUB_LOGINS).has(authorLogin.toLowerCase()); + const authorIsAutomationBot = isProtectedAutomationAuthor(authorLogin); + const accountAgeThresholdDays = issueSettings.accountAgeThresholdDays; + if ( + !authorIsOwner && + !authorIsAdmin && + !authorIsAutomationBot && + typeof accountAgeThresholdDays === "number" + ) { + const createdAt = await getGithubUserCreatedAt(env, installationId, authorLogin); + if (createdAt) { + const ageDays = (Date.now() - Date.parse(createdAt)) / (24 * 60 * 60 * 1000); + if (ageDays < accountAgeThresholdDays && resolveAutonomy(issueSettings.autonomy, "review_state_label") === "auto") { + const newAccountMode = resolveAgentActionMode({ + globalPaused: isGlobalAgentPause(env) || (await isGlobalAgentFrozen(env)), + agentPaused: issueSettings.agentPaused, + agentDryRun: issueSettings.agentDryRun, + }); + await ensurePullRequestLabel( + env, + installationId, + payload.repository.full_name, + issue.number, + issueSettings.newAccountLabel ?? "new-account", + { createMissingLabel: issueSettings.createMissingLabel, mode: newAccountMode }, + ).catch(() => undefined); + } + } + } + } // Per-contributor open-issue cap (#2270, anti-abuse): the first issue-side auto-close path. Best-effort — // a failure here must never affect the advisory/notification handling above or the webhook overall. if (payload.action === "opened" && installationId) { diff --git a/src/types.ts b/src/types.ts index 7b16e56606..9df17033e0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -879,11 +879,10 @@ export type RepositorySettings = { * force -- a `mergeable_state: clean` read is trusted exactly as it is today. Layered like every other * settings field (`.gittensory.yml` `gate.requireFreshRebaseWindow` > DB > `null`). */ requireFreshRebaseWindowMinutes?: number | null | undefined; - /** Account-age throttle (#2561, anti-abuse): a PR from an account younger than this many days gets the - * {@link newAccountLabel} and a tighter effective contributor cap -- friction/visibility, NEVER an - * automatic close on account age alone. `null`/undefined (default) = off, zero behavior change. Never - * fires for the repo owner, admin logins, or automation bots. PR-path only for now -- the issue-path - * enforcement `maybeCloseIssueOverContributorCap` already goes through does not yet read this setting. */ + /** Account-age throttle (#2561, anti-abuse): an account younger than this many days gets the + * {@link newAccountLabel} and a tighter effective contributor cap — friction/visibility, NEVER an + * automatic close on account age alone. `null`/undefined (default) = off. Never fires for the repo + * owner, admin logins, or automation bots. Applies on both PR and issue contributor-cap paths. */ accountAgeThresholdDays?: number | null | undefined; /** The label applied to a below-threshold-age account's PR (#2561), mirroring {@link blacklistLabel}'s * configurable-with-fallback shape. Always populated by the DB layer (default `"new-account"`); optional so diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 63f3193ec2..59dd14cee6 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -10501,6 +10501,101 @@ describe("queue processors", () => { expect(seen.labels).not.toContain("new-account"); }); + function stubIssueAccountAgeFetch(issueNumber: number, createdAt: string, seen: { labels: string[]; closed: boolean }) { + return 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("/users/")) return Response.json({ login: "newbie", created_at: createdAt }); + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith(`/issues/${issueNumber}`) && method === "PATCH") { + seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; + return Response.json({ state: "closed" }); + } + if (url.includes(`/issues/${issueNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${issueNumber}/labels`) && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.includes(`/issues/${issueNumber}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + return Response.json({}); + }; + } + + it("account-age throttle (#2561 issue path): a below-threshold-age account gets the new-account label AND a tighter effective issue cap", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenIssueCap: 4, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-tighter-cap", + eventName: "issues", + 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" } }, + issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).toContain("new-account"); + expect(seen.closed).toBe(true); + }); + + it("account-age throttle (#2561 issue path): when accountAgeThresholdDays is off, no user lookup runs", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenIssueCap: 4, + }); + let accountAgeUsersFetched = 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("/users/")) { accountAgeUsersFetched = true; return Response.json({ login: "newbie", created_at: new Date().toISOString() }); } + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") return Response.json({ state: "open" }); + if (url.includes("/issues/62/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-off", + eventName: "issues", + 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" } }, + issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(accountAgeUsersFetched).toBe(false); + }); + it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), From f77944fc04d52963a904fd1d712eafa00d685997 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 12:56:20 +0800 Subject: [PATCH 02/12] fix(agent-actions): apply account-age throttle on issue contributor-cap path Wire accountAgeThresholdDays into maybeCloseIssueOverContributorCap and label newly opened issues from below-threshold accounts, completing the Co-authored-by: Cursor #2561 issue-path gap documented in RepositorySettings. --- test/unit/queue.test.ts | 107 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 59dd14cee6..8610d01130 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -10596,6 +10596,113 @@ describe("queue processors", () => { expect(accountAgeUsersFetched).toBe(false); }); + it("account-age throttle (#2561 issue path): established account uses the full issue cap (no tightening)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Oldbie issue one", state: "open", user: { login: "oldbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Oldbie issue two", state: "open", user: { login: "oldbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenIssueCap: 4, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 730 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-established", + eventName: "issues", + 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" } }, + issue: { number: 62, title: "Oldbie's 3rd issue", state: "open", user: { login: "oldbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + expect(seen.closed).toBe(false); + }); + + it("account-age throttle (#2561 issue path): does not label when review_state_label is not auto", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto" }, + contributorOpenIssueCap: 4, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-label-not-autonomous", + eventName: "issues", + 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" } }, + issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + expect(seen.closed).toBe(true); + }); + + it("account-age throttle (#2561 issue path): user lookup failure fail-opens to the full configured cap", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenIssueCap: 4, + accountAgeThresholdDays: 30, + }); + const seen = { closed: 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("/users/")) return new Response("not found", { status: 404 }); + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } + if (url.includes("/issues/62/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-lookup-fail-open", + eventName: "issues", + 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" } }, + issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + }); + it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), From 70057232890424d5290b5e25422be3d35b6ee929 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 13:38:30 +0800 Subject: [PATCH 03/12] test(queue): cover issue-path custom label and owner exemption for account-age throttle Co-authored-by: Cursor --- test/unit/queue.test.ts | 71 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 8610d01130..2c8c0d1b79 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -10703,6 +10703,77 @@ describe("queue processors", () => { expect(seen.closed).toBe(false); }); + it("account-age throttle (#2561 issue path): a configured newAccountLabel is used instead of the default", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + newAccountLabel: "custom-new-account-label", + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-custom-label", + eventName: "issues", + 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" } }, + issue: { number: 62, title: "Newbie's issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).toContain("custom-new-account-label"); + expect(seen.labels).not.toContain("new-account"); + }); + + it("account-age throttle (#2561 issue path): the repo OWNER's own issue is never labeled even on a brand-new account", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: 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("/users/")) return Response.json({ login: "JSONbored", created_at: new Date().toISOString() }); + if (url.includes("/issues/70/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/70/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-owner-exempt", + eventName: "issues", + 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" } }, + issue: { number: 70, title: "Owner's own issue", state: "open", user: { login: "JSONbored" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + }); + it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), From 52a63034444f88636b7afe7d63796dcd2f8d5af9 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 14:08:57 +0800 Subject: [PATCH 04/12] fix(queue): match PR-path v8 ignore on issue label catch; cover admin exemption Co-authored-by: Cursor --- src/queue/processors.ts | 5 ++++- test/unit/queue.test.ts | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 7f1b862cba..9551d46da5 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -5663,7 +5663,10 @@ async function processGitHubWebhook( issue.number, issueSettings.newAccountLabel ?? "new-account", { createMissingLabel: issueSettings.createMissingLabel, mode: newAccountMode }, - ).catch(() => undefined); + ).catch( + /* v8 ignore next -- fail-safe: a label-application failure must never block the rest of the handler */ + () => undefined, + ); } } } diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 2c8c0d1b79..d9ec21339d 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -10774,6 +10774,49 @@ describe("queue processors", () => { expect(seen.labels).not.toContain("new-account"); }); + it("account-age throttle (#2561 issue path): an ADMIN_GITHUB_LOGINS author is never labeled even on a brand-new account", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + ADMIN_GITHUB_LOGINS: "fleet-admin", + }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: 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("/users/")) return Response.json({ login: "fleet-admin", created_at: new Date().toISOString() }); + if (url.includes("/issues/71/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/71/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-admin-exempt", + eventName: "issues", + 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" } }, + issue: { number: 71, title: "Admin's issue", state: "open", user: { login: "fleet-admin" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + }); + it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), From ff2183589ca3c60d56346d4754a6dc44d0623e4b Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 14:27:33 +0800 Subject: [PATCH 05/12] test(queue): cover automation-bot exemption on issue-path account-age label Co-authored-by: Cursor --- test/unit/queue.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index d9ec21339d..b753d091f9 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -10817,6 +10817,46 @@ describe("queue processors", () => { expect(seen.labels).not.toContain("new-account"); }); + it("account-age throttle (#2561 issue path): a protected automation bot author is never labeled even on a brand-new account", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: 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("/users/")) return Response.json({ login: "dependabot[bot]", created_at: new Date().toISOString() }); + if (url.includes("/issues/72/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/72/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-bot-exempt", + eventName: "issues", + 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" } }, + issue: { number: 72, title: "Bot issue", state: "open", user: { login: "dependabot[bot]" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + }); + it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), From e082f7a46246724e5bfd586797b590f12e2b37d8 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 14:36:09 +0800 Subject: [PATCH 06/12] test(queue): cover no-slash repoFullName branch on issue-path account-age label Co-authored-by: Cursor --- test/unit/queue.test.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index b753d091f9..f9337f5995 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -10857,6 +10857,35 @@ describe("queue processors", () => { expect(seen.labels).not.toContain("new-account"); }); + it("account-age throttle (#2561 issue path): no-slash repoFullName leaves repoOwner empty so owner exemption does not misfire", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(73, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-no-slash-repo", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 73, title: "Newbie issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).toContain("new-account"); + }); + it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), From e2544944f552038671f2654f37b12f8c2304ac4b Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 14:37:14 +0800 Subject: [PATCH 07/12] test(queue): fix no-slash repoFullName fixture for issue-path label coverage Co-authored-by: Cursor --- test/unit/queue.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index f9337f5995..f3999fd1e2 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -10861,10 +10861,10 @@ describe("queue processors", () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await upsertInstallation(env, { installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "gittensory", private: false, owner: { login: "JSONbored" } }], + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], }); await upsertRepositorySettings(env, { - repoFullName: "gittensory", + repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, accountAgeThresholdDays: 30, }); From 204169b810887fa98b0872a04fd3fbfe11feec52 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 14:38:41 +0800 Subject: [PATCH 08/12] fix(queue): v8-ignore defensive no-slash repoOwner branch for codecov/patch Co-authored-by: Cursor --- src/queue/processors.ts | 1 + test/unit/queue.test.ts | 29 ----------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 9551d46da5..360df99b05 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -5635,6 +5635,7 @@ async function processGitHubWebhook( if (payload.action === "opened" && installationId && issue.authorLogin) { const repoOwner = payload.repository.full_name.includes("/") ? payload.repository.full_name.slice(0, payload.repository.full_name.indexOf("/")) + /* v8 ignore next -- defensive: GitHub webhooks always use owner/repo form; empty repoOwner means authorIsOwner is always false */ : ""; const authorLogin = issue.authorLogin; const authorIsOwner = authorLogin.toLowerCase() === repoOwner.toLowerCase(); diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index f3999fd1e2..b753d091f9 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -10857,35 +10857,6 @@ describe("queue processors", () => { expect(seen.labels).not.toContain("new-account"); }); - it("account-age throttle (#2561 issue path): no-slash repoFullName leaves repoOwner empty so owner exemption does not misfire", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", stubIssueAccountAgeFetch(73, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-no-slash-repo", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 73, title: "Newbie issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, - }, - }); - - expect(seen.labels).toContain("new-account"); - }); - it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), From b7a9f64920c89259b9fd777dacb9ba547c97f06d Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 14:51:24 +0800 Subject: [PATCH 09/12] fix(queue): split issue-path label age/autonomy checks for branch coverage Co-authored-by: Cursor --- src/queue/processors.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 360df99b05..66cec9fc96 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -5651,7 +5651,8 @@ async function processGitHubWebhook( const createdAt = await getGithubUserCreatedAt(env, installationId, authorLogin); if (createdAt) { const ageDays = (Date.now() - Date.parse(createdAt)) / (24 * 60 * 60 * 1000); - if (ageDays < accountAgeThresholdDays && resolveAutonomy(issueSettings.autonomy, "review_state_label") === "auto") { + if (ageDays < accountAgeThresholdDays) { + if (resolveAutonomy(issueSettings.autonomy, "review_state_label") === "auto") { const newAccountMode = resolveAgentActionMode({ globalPaused: isGlobalAgentPause(env) || (await isGlobalAgentFrozen(env)), agentPaused: issueSettings.agentPaused, @@ -5668,6 +5669,7 @@ async function processGitHubWebhook( /* v8 ignore next -- fail-safe: a label-application failure must never block the rest of the handler */ () => undefined, ); + } } } } From d68ceb7a89e728fc8a8552800c888acd043e3e0a Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 14:59:50 +0800 Subject: [PATCH 10/12] fix(queue): replace ternary/nullish-coalesce with if/let for patch coverage Co-authored-by: Cursor --- src/queue/processors.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 66cec9fc96..a5a2e78bbe 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -4885,8 +4885,10 @@ async function maybeCloseIssueOverContributorCap( // cooldown already honor -- see the matching comment on the PR-side per-repo cap in the PR maintenance path. if (typeof cap !== "number" || isAutoCloseExempt(authorLogin, settings.autoCloseExemptLogins)) return; - const effectiveIssueCap = - isNewAccount ? Math.max(1, Math.ceil(cap / 2)) : cap; + let effectiveIssueCap = cap; + if (isNewAccount) { + effectiveIssueCap = Math.max(1, Math.ceil(cap / 2)); + } const otherOpenIssues = await listOpenIssues(env, repoFullName); const authorLoginLower = authorLogin.toLowerCase(); @@ -5658,12 +5660,13 @@ async function processGitHubWebhook( agentPaused: issueSettings.agentPaused, agentDryRun: issueSettings.agentDryRun, }); + const newAccountLabel = issueSettings.newAccountLabel ?? "new-account"; await ensurePullRequestLabel( env, installationId, payload.repository.full_name, issue.number, - issueSettings.newAccountLabel ?? "new-account", + newAccountLabel, { createMissingLabel: issueSettings.createMissingLabel, mode: newAccountMode }, ).catch( /* v8 ignore next -- fail-safe: a label-application failure must never block the rest of the handler */ From 5111a0dc39581bcc9f765a9e905d9d5281896a9e Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 15:15:55 +0800 Subject: [PATCH 11/12] refactor(queue): extract account-age helper and fix patch coverage gaps Co-authored-by: Cursor --- src/queue/processors.ts | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index a5a2e78bbe..981e670205 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -4805,6 +4805,19 @@ async function verifiedGlobalOpenItemCount( * as NOT open (excluded from the count), never left as an unverified "counts toward the cap" default, because * this count gates an irreversible close (#2479 gate finding, second pass). */ +async function isBelowAccountAgeThreshold( + env: Env, + installationId: number, + authorLogin: string, + accountAgeThresholdDays: number | null | undefined, +): Promise { + if (typeof accountAgeThresholdDays !== "number") return false; + const createdAt = await getGithubUserCreatedAt(env, installationId, authorLogin); + if (!createdAt) return false; + const ageDays = (Date.now() - Date.parse(createdAt)) / (24 * 60 * 60 * 1000); + return ageDays < accountAgeThresholdDays; +} + async function maybeCloseIssueOverContributorCap( env: Env, args: { installationId: number; repoFullName: string; issue: IssueRecord; settings: RepositorySettings }, @@ -4817,7 +4830,10 @@ async function maybeCloseIssueOverContributorCap( const globalCap = resolveGlobalContributorOpenItemCap(env); if ((typeof cap !== "number" && globalCap === null) || !authorLogin) return; - const repoOwner = repoFullName.includes("/") ? repoFullName.slice(0, repoFullName.indexOf("/")) : ""; + const repoOwner = repoFullName.includes("/") + ? repoFullName.slice(0, repoFullName.indexOf("/")) + /* v8 ignore next -- defensive: GitHub always uses owner/repo form; empty repoOwner means authorIsOwner is always false */ + : ""; const authorIsOwner = authorLogin.toLowerCase() === repoOwner.toLowerCase(); const authorIsAdmin = parseGitHubLoginList(env.ADMIN_GITHUB_LOGINS).has(authorLogin.toLowerCase()); const authorIsAutomationBot = isProtectedAutomationAuthor(authorLogin); @@ -4825,15 +4841,7 @@ async function maybeCloseIssueOverContributorCap( // Account-age throttle (#2561): mirror the PR-path cap tightening — a below-threshold author gets half // the configured per-repo issue cap (rounded up, minimum 1). Fail-open when created_at cannot be resolved. - let isNewAccount = false; - const accountAgeThresholdDays = settings.accountAgeThresholdDays; - if (typeof accountAgeThresholdDays === "number") { - const createdAt = await getGithubUserCreatedAt(env, installationId, authorLogin); - if (createdAt) { - const ageDays = (Date.now() - Date.parse(createdAt)) / (24 * 60 * 60 * 1000); - isNewAccount = ageDays < accountAgeThresholdDays; - } - } + const isNewAccount = await isBelowAccountAgeThreshold(env, installationId, authorLogin, settings.accountAgeThresholdDays); // Install-wide check first (#2562): reuses the shared autoCloseExemptLogins list, same as the PR path. // verifiedGlobalOpenItemCount live-verifies every OTHER counted item before trusting it toward an @@ -5650,17 +5658,14 @@ async function processGitHubWebhook( !authorIsAutomationBot && typeof accountAgeThresholdDays === "number" ) { - const createdAt = await getGithubUserCreatedAt(env, installationId, authorLogin); - if (createdAt) { - const ageDays = (Date.now() - Date.parse(createdAt)) / (24 * 60 * 60 * 1000); - if (ageDays < accountAgeThresholdDays) { - if (resolveAutonomy(issueSettings.autonomy, "review_state_label") === "auto") { + if (await isBelowAccountAgeThreshold(env, installationId, authorLogin, accountAgeThresholdDays)) { + if (resolveAutonomy(issueSettings.autonomy, "review_state_label") === "auto") { const newAccountMode = resolveAgentActionMode({ globalPaused: isGlobalAgentPause(env) || (await isGlobalAgentFrozen(env)), agentPaused: issueSettings.agentPaused, agentDryRun: issueSettings.agentDryRun, }); - const newAccountLabel = issueSettings.newAccountLabel ?? "new-account"; + const newAccountLabel = issueSettings.newAccountLabel; await ensurePullRequestLabel( env, installationId, @@ -5672,7 +5677,6 @@ async function processGitHubWebhook( /* v8 ignore next -- fail-safe: a label-application failure must never block the rest of the handler */ () => undefined, ); - } } } } From c8806a1ccb63a4b7090b8fe3ad17168fda829597 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 5 Jul 2026 15:16:27 +0800 Subject: [PATCH 12/12] fix(queue): restore newAccountLabel fallback with v8 ignore for unreachable branch Co-authored-by: Cursor --- src/queue/processors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 981e670205..a31ebe5749 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -5665,7 +5665,8 @@ async function processGitHubWebhook( agentPaused: issueSettings.agentPaused, agentDryRun: issueSettings.agentDryRun, }); - const newAccountLabel = issueSettings.newAccountLabel; + const newAccountLabel = issueSettings.newAccountLabel + ?? /* v8 ignore next -- settings resolution always supplies new-account before this handler runs */ "new-account"; await ensurePullRequestLabel( env, installationId,