diff --git a/src/env.d.ts b/src/env.d.ts index 36c45b03c6..fb965729e5 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -125,11 +125,18 @@ declare global { * admin/bot contributor may have open ACROSS EVERY repo this install gates, combined. Purely an * install-scoped aggregate over this same database (no cross-instance networking) -- catches an actor * spreading low-volume spam/farming PRs across several gated repos in one self-hosted install, which no - * single repo's own contributorOpenPrCap/contributorOpenIssueCap can see. Unset/invalid (the default) = no - * cap, byte-identical to today. Checked IN ADDITION TO (not instead of) the existing per-repo caps, in the + * single repo's own contributorOpenPrCap/contributorOpenIssueCap can see. Unset/invalid falls back to a + * real default (20) rather than "no cap" (#4511) -- set to the literal string "off" for the old + * unconditional-no-cap behavior. Checked IN ADDITION TO (not instead of) the existing per-repo caps, in the * same contributor_cap short-circuit (src/settings/agent-actions.ts). A positive integer string (e.g. "20"); * see src/settings/global-contributor-cap.ts for parsing. */ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP?: string; + /** Same shape as {@link GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP}, but for a CONFIRMED official Gittensor miner + * specifically (#4511) -- a verified miner identity gets this cap instead of the human one, since a + * legitimate fleet spread across many repos in one install is expected to run more concurrent open items + * than a single human contributor. Unset/invalid falls back to a higher real default (50); "off" exempts + * confirmed miners from the install-wide cap entirely while humans stay capped. */ + GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER?: string; /** Install-wide default for the per-repo contributorCapCancelCi setting (#2462): "true"/"1"/"yes"/"on" * (case-insensitive) enables cancelling in-flight CI runs on a contributor_cap close for every repo that * hasn't explicitly configured its own value. Unset/blank/anything else = off (the existing behavior). A diff --git a/src/queue/processors.ts b/src/queue/processors.ts index c0256c5448..2c9af786f6 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -296,7 +296,7 @@ import { type PlannedAgentAction, } from "../settings/agent-actions"; import { isAutoCloseExempt } from "../settings/auto-close-exempt"; -import { resolveGlobalContributorOpenItemCap } from "../settings/global-contributor-cap"; +import { resolveGlobalContributorOpenItemCap, resolveGlobalContributorOpenItemCapForMiner } from "../settings/global-contributor-cap"; import { detectMigrationCollisions, extractMigrationNumber, KNOWN_MIGRATION_DUPLICATES } from "../db/migration-collisions"; import { listMigrationFilenamesAtRef } from "../github/migration-tree"; import { @@ -3054,12 +3054,27 @@ async function runAgentMaintenancePlanAndExecute( // Install-wide contributor open-item cap (#2562, anti-abuse): IN ADDITION TO the per-repo cap above, not // instead of it -- only evaluated when the per-repo cap didn't already match (short-circuit: no need for a - // second cross-repo DB read once this PR is already being closed). Off by default (resolveGlobalContributorOpenItemCap - // returns null when GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP is unset/invalid) ⇒ zero extra queries, zero behavior - // change for an install that hasn't opted in. Reuses the shared autoCloseExemptLogins list (#2463) so a - // maintainer-named login is exempt here exactly like the per-repo caps and review-nag cooldown. - if (contributorCapMatch === undefined && pr.authorLogin && !isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins)) { - const globalCap = resolveGlobalContributorOpenItemCap(env); + // second cross-repo DB read once this PR is already being closed). Defaults to a real cap even when unset + // (#4511) -- a CONFIRMED official Gittensor miner gets its own, higher fleet-appropriate default instead of + // either "no cap" or the plain human default, since a legitimate fleet spread across many repos in one + // install is expected to run more concurrent open items than a single human contributor. Reuses the shared + // autoCloseExemptLogins list (#2463) so a maintainer-named login is exempt here exactly like the per-repo + // caps and review-nag cooldown. + const prGlobalCapForHuman = resolveGlobalContributorOpenItemCap(env); + const prGlobalCapForMiner = resolveGlobalContributorOpenItemCapForMiner(env); + if ( + contributorCapMatch === undefined && + pr.authorLogin && + (prGlobalCapForHuman !== null || prGlobalCapForMiner !== null) && + !isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins) + ) { + // Deferred until we know at least one of the two resolvers is actually active (#4511): the identity + // lookup below is cached but still a DB/network round trip, and both resolvers above are plain env reads. + const officialMiner = await getCachedOfficialMinerDetection(env, pr.authorLogin, { + targetKey: `${repoFullName}#${pr.number}`, + deliveryId, + }); + const globalCap = officialMiner.status === "confirmed" ? prGlobalCapForMiner : prGlobalCapForHuman; if (globalCap !== null) { const globalOpenCount = await verifiedGlobalOpenItemCount(env, installationId, pr.authorLogin, { repoFullName, @@ -5406,15 +5421,19 @@ async function verifiedGlobalOpenItemCount( */ async function maybeCloseIssueOverContributorCap( env: Env, - args: { installationId: number; repoFullName: string; issue: IssueRecord; settings: RepositorySettings }, + args: { installationId: number; repoFullName: string; issue: IssueRecord; settings: RepositorySettings; deliveryId: string }, ): Promise { - const { installationId, repoFullName, issue, settings } = args; + const { installationId, repoFullName, issue, settings, deliveryId } = args; const cap = settings.contributorOpenIssueCap; const authorLogin = issue.authorLogin; // Install-wide cap (#2562) is checked IN ADDITION TO the per-repo cap, so this function must still run when - // ONLY the global cap is configured (the per-repo cap stays optional/off, its usual default). - const globalCap = resolveGlobalContributorOpenItemCap(env); - if ((typeof cap !== "number" && globalCap === null) || !authorLogin) return; + // ONLY a global cap is configured (the per-repo cap stays optional/off, its usual default). Both global + // resolvers now default to a real number even when unset (#4511) -- a CONFIRMED official Gittensor miner + // gets its own fleet-appropriate cap instead of the human one, checked separately below once we know at + // least one of the two is active (both resolvers here are plain env reads; the identity check isn't). + const globalCapForHuman = resolveGlobalContributorOpenItemCap(env); + const globalCapForMiner = resolveGlobalContributorOpenItemCapForMiner(env); + if ((typeof cap !== "number" && globalCapForHuman === null && globalCapForMiner === null) || !authorLogin) return; const repoOwner = repoOwnerLoginFromFullName(repoFullName); const authorIsOwner = authorLogin.toLowerCase() === repoOwner.toLowerCase(); @@ -5429,7 +5448,13 @@ async function maybeCloseIssueOverContributorCap( // 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. - if (globalCap !== null && !isAutoCloseExempt(authorLogin, settings.autoCloseExemptLogins)) { + if ((globalCapForHuman !== null || globalCapForMiner !== null) && !isAutoCloseExempt(authorLogin, settings.autoCloseExemptLogins)) { + const officialMiner = await getCachedOfficialMinerDetection(env, authorLogin, { + targetKey: `${repoFullName}#${issue.number}`, + deliveryId, + }); + const globalCap = officialMiner.status === "confirmed" ? globalCapForMiner : globalCapForHuman; + if (globalCap === null) return; const globalOpenCount = await verifiedGlobalOpenItemCount(env, installationId, authorLogin, { repoFullName, number: issue.number, @@ -6396,6 +6421,7 @@ async function processGitHubWebhook( repoFullName: payload.repository.full_name, issue, settings: issueSettings, + deliveryId, }).catch((error) => { /* v8 ignore next -- best-effort: an issue-cap enforcement failure is logged, never surfaced to the webhook. */ console.error( diff --git a/src/settings/global-contributor-cap.ts b/src/settings/global-contributor-cap.ts index 126f0ac898..a60fc79af3 100644 --- a/src/settings/global-contributor-cap.ts +++ b/src/settings/global-contributor-cap.ts @@ -6,19 +6,50 @@ // already tracks. Deliberately an env var (not a per-repo `.gittensory.yml`/DB field like the caps above) -- // this setting aggregates ACROSS repos, so it cannot be "this repo's" setting; it belongs to the install as a // whole, mirroring how global_contributor_blacklist is a tenant-free singleton rather than a per-repo column. -// Off by default (unset/invalid ⇒ null ⇒ no cap): zero behavior change for a single-repo install or one that -// hasn't opted in. +// +// #4511 (AMS-readiness follow-up): "unset ⇒ null ⇒ no cap" was the ONLY defense against one identity farming +// PRs across every gated repo in an install, and it was off unless an operator proactively opted in AND +// remembered to pre-size it. That's backwards for a fleet-scale actor -- fail-safe means a sane cap exists by +// default, not that protection is silently absent until someone configures it. So: unset/malformed now falls +// back to a real default (DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP) rather than "no cap" -- this IS a behavior +// change for any install that never set the env var. An operator who genuinely wants no cap sets the env var +// to the literal string "off" (a load-bearing explicit opt-out, distinct from "unset"), mirroring the +// explicit-null-means-something idiom used elsewhere in this codebase (e.g. blacklistLabel). const GLOBAL_ENV_KEY = "GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP"; +const GLOBAL_MINER_ENV_KEY = "GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER"; +const OFF_SENTINEL = "off"; -/** Parse+validate the install-wide open-item cap from env. Same non-rounding shape as the per-repo caps' - * normalizeOpenItemCap (db/repositories.ts): a discrete count of open items, not a score, so a - * fractional/non-positive/non-numeric value is a malformed cap and is dropped to `null` (no cap) rather than - * coerced into a nonsensical threshold. Never throws. Unlike the per-repo cap, this install-wide cap is not clamped to the - * per-repo live-check budget because the install-wide verifier loads and verifies a larger row set. */ -export function resolveGlobalContributorOpenItemCap(env: { GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP?: string | undefined }): number | null { - const raw = env[GLOBAL_ENV_KEY]; - if (typeof raw !== "string" || raw.trim() === "") return null; +/** Default install-wide cap for a non-miner actor when {@link GLOBAL_ENV_KEY} is unset or malformed (#4511). */ +export const DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP = 20; +/** Default install-wide cap for a CONFIRMED official Gittensor miner (#4511): higher than the human default + * because a legitimate fleet spread across many repos in one install is expected to run more concurrent open + * items than a single human contributor, without being unlimited. Applies ONLY once the author is verified + * via the same official-miner-detection path the rest of the codebase already trusts for this purpose + * (getCachedOfficialMinerDetection) -- an unverified/unconfirmed actor always gets the human default. */ +export const DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER = 50; + +function resolveCapEnv(raw: string | undefined, fallback: number): number | null { + if (typeof raw !== "string" || raw.trim() === "") return fallback; + if (raw.trim().toLowerCase() === OFF_SENTINEL) return null; const parsed = Number(raw); - if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) return null; + // A malformed value (fractional/non-positive/non-numeric) falls back to the SAME default an unset env var + // would use, not to "no cap" -- a typo in an operator's .env must never silently disable this defense. + if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) return fallback; return parsed; } + +/** Resolve the install-wide open-item cap for an ordinary (non-miner) actor. `null` means explicitly disabled + * (env var set to `"off"`) -- everything else, including unset, resolves to a real number. Never throws. + * Unlike the per-repo cap, this install-wide cap is not clamped to the per-repo live-check budget because the + * install-wide verifier loads and verifies a larger row set. */ +export function resolveGlobalContributorOpenItemCap(env: { GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP?: string | undefined }): number | null { + return resolveCapEnv(env[GLOBAL_ENV_KEY], DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); +} + +/** Resolve the install-wide open-item cap for a CONFIRMED official Gittensor miner (#4511) -- same shape and + * `"off"` escape hatch as {@link resolveGlobalContributorOpenItemCap}, but with a fleet-appropriate default. + * Callers must only use this once the actor's miner status is independently verified; this function does not + * itself check identity. */ +export function resolveGlobalContributorOpenItemCapForMiner(env: { GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER?: string | undefined }): number | null { + return resolveCapEnv(env[GLOBAL_MINER_ENV_KEY], DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER); +} diff --git a/test/unit/global-contributor-cap.test.ts b/test/unit/global-contributor-cap.test.ts index 7a33aab578..41aab2975c 100644 --- a/test/unit/global-contributor-cap.test.ts +++ b/test/unit/global-contributor-cap.test.ts @@ -1,12 +1,17 @@ import { describe, expect, it } from "vitest"; -import { resolveGlobalContributorOpenItemCap } from "../../src/settings/global-contributor-cap"; +import { + DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP, + DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER, + resolveGlobalContributorOpenItemCap, + resolveGlobalContributorOpenItemCapForMiner, +} from "../../src/settings/global-contributor-cap"; import { listOpenItemsForAuthorAcrossInstall, upsertIssueFromGitHub, upsertPullRequestFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories"; import { createTestEnv } from "../helpers/d1"; -describe("resolveGlobalContributorOpenItemCap (#2562)", () => { - it("is off by default when the env var is unset", () => { - expect(resolveGlobalContributorOpenItemCap({})).toBeNull(); - expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: undefined })).toBeNull(); +describe("resolveGlobalContributorOpenItemCap (#2562, #4511)", () => { + it("falls back to the real default when the env var is unset (no longer 'no cap')", () => { + expect(resolveGlobalContributorOpenItemCap({})).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: undefined })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); }); it("parses a valid positive-integer string", () => { @@ -20,13 +25,38 @@ describe("resolveGlobalContributorOpenItemCap (#2562)", () => { expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "100" })).toBe(100); }); - it("drops a fractional/non-positive/non-numeric value to null (no cap), never coerced", () => { - expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2.5" })).toBeNull(); - expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "0" })).toBeNull(); - expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "-3" })).toBeNull(); - expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "not-a-number" })).toBeNull(); - expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "" })).toBeNull(); - expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: " " })).toBeNull(); + it("falls back to the default (not null) on a fractional/non-positive/non-numeric value -- a typo must never silently disable this defense", () => { + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2.5" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "0" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "-3" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "not-a-number" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: " " })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); + }); + + it("the literal string 'off' (any case) is the explicit escape hatch back to no cap", () => { + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "off" })).toBeNull(); + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "OFF" })).toBeNull(); + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: " Off " })).toBeNull(); + }); +}); + +describe("resolveGlobalContributorOpenItemCapForMiner (#4511)", () => { + it("falls back to the higher miner default when unset", () => { + expect(resolveGlobalContributorOpenItemCapForMiner({})).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER); + expect(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER).toBeGreaterThan(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP); + }); + + it("parses a valid override independently of the human cap var", () => { + expect(resolveGlobalContributorOpenItemCapForMiner({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER: "75" })).toBe(75); + }); + + it("falls back to the miner default (not null) on a malformed value", () => { + expect(resolveGlobalContributorOpenItemCapForMiner({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER: "nope" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER); + }); + + it("'off' exempts confirmed miners from the install-wide cap entirely", () => { + expect(resolveGlobalContributorOpenItemCapForMiner({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER: "off" })).toBeNull(); }); }); diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 790e307359..ce399fe848 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -13853,8 +13853,8 @@ describe("queue processors", () => { expect(seen.livePullReads).not.toContain(11); }); - it("install-wide contributor open-item cap (#2562): off by default (env var unset) — a spread-across-repos actor is never closed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // no GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP + it("install-wide contributor open-item cap (#2562, #4511): env var unset falls back to the real default (20), so a spread-across-repos actor well under it is not closed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // no GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP -- resolves to the DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP, not "no cap" (#4511) await upsertInstallation(env, { installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, repositories: [ @@ -14030,6 +14030,71 @@ describe("queue processors", () => { expect(seen.closed).toBe(false); }); + it("install-wide contributor open-item cap (#4511): a CONFIRMED official Gittensor miner gets the higher miner-specific cap, not the human one, even though the human cap alone would already be exceeded", async () => { + // Human cap (2) would already be exceeded by 3 open items -- but farmer99 resolves as a confirmed miner via + // the /miners API, so the fleet-appropriate default (50, GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER unset) applies + // instead, and 3 is nowhere near that. Must fall through without matching. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 11, title: "Farmer 2nd PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb11" }, labels: [], body: "z" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/repo-a", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "farmer99", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://api.gittensor.io/miners/123") return Response.json({}); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/10") || url.endsWith("/pulls/11")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-cap-confirmed-miner", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Confirmed miner's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + expect(seen.closed).toBe(false); + }); + it("contributor open-PR cap (#2270): the repo OWNER's own PR is never closed even over the cap (live processor path, not just the planner)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await upsertInstallation(env, { @@ -15414,8 +15479,8 @@ describe("queue processors", () => { expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open pull requests and issues") && c.includes("across every repository it gates"))).toBe(true); }); - it("install-wide contributor open-item cap (#2562): off by default (env var unset) — an issue author spread across repos is never closed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // no GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP + it("install-wide contributor open-item cap (#2562, #4511): env var unset falls back to the real default (20), so an issue author spread across repos well under it is not closed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // no GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP -- resolves to the DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP, not "no cap" (#4511) 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: [