From 0dd0a1c87de8c6ec8bf11e0325cca46a1eceee26 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 7 Jul 2026 02:26:33 -0700 Subject: [PATCH] fix(settings): bound the install-wide contributor open-item cap resolveGlobalContributorOpenItemCap (#2562) validated but never clamped its return value, so GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP could be set above the fixed 100-row live-verification sample budget and remain just as unenforceable as the per-repo caps were before that fix. --- src/settings/global-contributor-cap.ts | 12 ++++++++---- test/unit/global-contributor-cap.test.ts | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/settings/global-contributor-cap.ts b/src/settings/global-contributor-cap.ts index 07a046acd9..9f9a605e08 100644 --- a/src/settings/global-contributor-cap.ts +++ b/src/settings/global-contributor-cap.ts @@ -8,16 +8,20 @@ // 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. +import { MAX_CONTRIBUTOR_OPEN_ITEM_CAP } from "../types"; + const GLOBAL_ENV_KEY = "GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP"; -/** Parse+validate the install-wide open-item cap from env. Same non-clamping, non-rounding shape as the - * per-repo caps' normalizeOpenItemCap (db/repositories.ts): a discrete count of open items, not a score, so a +/** 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. */ + * coerced into a nonsensical threshold. Never throws. Clamped to {@link MAX_CONTRIBUTOR_OPEN_ITEM_CAP} for the + * same reason normalizeOpenItemCap is: live enforcement only ever samples a fixed 100-row budget, so a + * configured value above that is silently unenforceable. */ 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; const parsed = Number(raw); if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) return null; - return parsed; + return Math.min(parsed, MAX_CONTRIBUTOR_OPEN_ITEM_CAP); } diff --git a/test/unit/global-contributor-cap.test.ts b/test/unit/global-contributor-cap.test.ts index f40af74fc0..58386be6ed 100644 --- a/test/unit/global-contributor-cap.test.ts +++ b/test/unit/global-contributor-cap.test.ts @@ -14,6 +14,13 @@ describe("resolveGlobalContributorOpenItemCap (#2562)", () => { expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "1" })).toBe(1); }); + // Live enforcement only ever samples a fixed 100-row budget (MAX_CONTRIBUTOR_OPEN_ITEM_CAP), so a configured + // value above that is silently unenforceable -- mirrors normalizeOpenItemCap's own clamp (db/repositories.ts). + it("clamps a value above the live-check sample budget to 100, and preserves exactly 100 unclamped", () => { + expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "500" })).toBe(100); + 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();