diff --git a/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.test.tsx b/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.test.tsx index 9272ee5908..34e87a6ef4 100644 --- a/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.test.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.test.tsx @@ -37,7 +37,6 @@ const ADVISORY_SETTINGS = { requireLinkedIssue: false, commandAuthorization: {}, autonomy: {}, - autoMaintain: { requireApprovals: 1, mergeMethod: "squash" as const }, agentPaused: false, agentDryRun: false, }; diff --git a/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.tsx b/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.tsx index f3769a2bd5..cc2d48e546 100644 --- a/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.tsx @@ -36,7 +36,6 @@ function normalizeLoadedSettings(data: MaintainerSettingsEditable): MaintainerSe autonomy: data.autonomy ?? {}, agentPaused: data.agentPaused ?? false, agentDryRun: data.agentDryRun ?? false, - autoMaintain: data.autoMaintain ?? { requireApprovals: 1, mergeMethod: "squash" }, }; } diff --git a/apps/loopover-ui/src/components/site/app-panels/maintainer-settings.tsx b/apps/loopover-ui/src/components/site/app-panels/maintainer-settings.tsx index 0c7ae19c0a..6433bd1c54 100644 --- a/apps/loopover-ui/src/components/site/app-panels/maintainer-settings.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/maintainer-settings.tsx @@ -8,7 +8,6 @@ import { extractPreviewRepoOptions, splitRepoFullName } from "@/lib/maintainer-s import { buildMaintainerSettingsSavePayload, type AgentActionClass, - type AutoMergeMethod, type AutonomyLevel, type CommandRole, type GateMode, @@ -195,10 +194,6 @@ export function MaintainerSettings({ reviewability }: { reviewability: Array<{ p autonomy: result.data.autonomy ?? {}, agentPaused: result.data.agentPaused ?? false, agentDryRun: result.data.agentDryRun ?? false, - autoMaintain: result.data.autoMaintain ?? { - requireApprovals: 1, - mergeMethod: "squash", - }, } : null, ); @@ -388,41 +383,8 @@ export function MaintainerSettings({ reviewability }: { reviewability: Array<{ p ))} -
- - -
+ {/* #6445: "Approvals before auto-merge" / "Merge method" removed -- autoMaintain is no longer + DB-backed, config-as-code only via .loopover.yml's settings: block now. */}
>; - autoMaintain: { requireApprovals: number; mergeMethod: AutoMergeMethod }; + // #6445: autoMaintain removed -- no longer DB-backed, config-as-code only via .loopover.yml's + // settings: block now (the dashboard can no longer write it). agentPaused: boolean; agentDryRun: boolean; }; @@ -61,7 +61,6 @@ export const MAINTAINER_SETTINGS_EDITABLE_KEYS: Array(value, null)).policy; } @@ -7798,14 +7746,6 @@ function parseContributorBlacklist(value: string): RepositorySettings["contribut return normalizeContributorBlacklist(parseJson(value, null)).entries; } -function parseAutoCloseExemptLogins(value: string): string[] { - return normalizeAutoCloseExemptLogins(parseJson(value, null)).logins; -} - -function normalizeReviewNagPolicy(value: string | null | undefined): "off" | "hold" | "close" { - return value === "hold" || value === "close" ? value : "off"; -} - function normalizeDraftPrClosePolicy(value: string | null | undefined): "off" | "close" { return value === "close" ? "close" : "off"; } @@ -7836,35 +7776,21 @@ function parseJsonStringArray(value: string): string[] { return parsed.filter((item): item is string => typeof item === "string" && item.trim().length > 0); } -function normalizeCommandRateLimitPolicy(value: string | null | undefined): "off" | "hold" { - return value === "hold" ? value : "off"; -} - function normalizeSkipAutomationBotAuthors(value: string | null | undefined): "inherit" | "off" | "enabled" { return value === "off" || value === "enabled" ? value : "inherit"; } -// A review-nag threshold/window is a discrete positive count, not a score — reuses the same non-clamping, -// non-rounding shape as contributorOpenPrCap's normalizeOpenItemCap (#2270): an invalid value (fractional, -// non-positive, non-finite) falls back to the given default rather than being silently coerced. +// Still used by the global moderation config's banThreshold (#selfhost-mod-engine): an invalid value +// (fractional, non-positive, non-finite) falls back to the given default rather than being silently coerced. function normalizePositiveIntWithDefault(value: number | null | undefined, fallback: number): number { if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) return fallback; return value; } -function normalizeReviewNagCooldownDays(value: number | null | undefined, fallback: number): number { - const normalized = normalizePositiveIntWithDefault(value, fallback); - return Math.min(normalized, MAX_REVIEW_NAG_COOLDOWN_DAYS); -} - function parseAutonomyPolicy(value: string): AutonomyPolicy { return normalizeAutonomyPolicy(parseJson(value, null)); } -function parseAutoMaintainPolicy(value: string): AutoMaintainPolicy { - return normalizeAutoMaintainPolicy(parseJson(value, null)); -} - function parseSyncStatus(value: string): RepoSyncStateRecord["status"] { if ( value === "running" || diff --git a/src/db/schema.ts b/src/db/schema.ts index 5931c364cd..f4681d525b 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -46,8 +46,6 @@ export const repositories = sqliteTable("repositories", { export const repositorySettings = sqliteTable("repository_settings", { repoFullName: text("repo_full_name").primaryKey(), reviewCheckMode: text("review_check_mode").notNull().default("disabled"), - projectMilestoneMatchMode: text("project_milestone_match_mode").notNull().default("off"), - autoProjectMilestoneMatchBackend: text("auto_project_milestone_match_backend").notNull().default("github"), gatePack: text("gate_pack").notNull().default("gittensor"), // Missing a linked issue is advisory-only by default -- issues aren't always available, so it only // blocks when a repo explicitly sets linkedIssueGateMode: "block". The sibling requireLinkedIssue @@ -92,41 +90,11 @@ export const repositorySettings = sqliteTable("repository_settings", { requireLinkedIssue: integer("require_linked_issue", { mode: "boolean" }).notNull().default(false), commandAuthorizationJson: text("command_authorization_json").notNull().default("{}"), autonomyJson: text("autonomy_json").notNull().default("{}"), - autoMaintainJson: text("auto_maintain_json").notNull().default("{}"), agentPaused: integer("agent_paused", { mode: "boolean" }).notNull().default(false), agentDryRun: integer("agent_dry_run", { mode: "boolean" }).notNull().default(false), - // Per-contributor open PR/issue caps (#2270, anti-abuse): null = no cap (default). Enforcement lands separately. - contributorOpenPrCap: integer("contributor_open_pr_cap"), - contributorOpenIssueCap: integer("contributor_open_issue_cap"), - contributorCapLabel: text("contributor_cap_label").notNull().default("over-contributor-limit"), - // Cancel in-flight CI runs on a contributor_cap close (#2462): null = unset, falls back to the - // CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT env var (nullable, unlike a plain boolean toggle, so an explicit `false` - // is distinguishable from "not configured" for that fallback). Only meaningful when the App installation has - // granted actions:write -- degrades gracefully (logs, never blocks the close) otherwise. - contributorCapCancelCi: integer("contributor_cap_cancel_ci", { mode: "boolean" }), - // Review-request nagging cooldown (#2463, anti-abuse): default 'off' (disabled). - reviewNagPolicy: text("review_nag_policy").notNull().default("off"), - reviewNagMaxPings: integer("review_nag_max_pings").notNull().default(3), - reviewNagCooldownDays: integer("review_nag_cooldown_days").notNull().default(5), - reviewNagLabel: text("review_nag_label").notNull().default("review-nag-cooldown"), - // Maintainer-mention nag moderation (#label-scoping): a JSON array of GitHub logins ALSO throttled under the - // review-nag cooldown above, on top of the bot's own `@loopover` handle. Default '[]' (no logins watched). - reviewNagMonitoredMentionsJson: text("review_nag_monitored_mentions_json").notNull().default("[]"), - // Shared repo-scoped exemption list (#2463): a JSON array of GitHub logins. - autoCloseExemptLoginsJson: text("auto_close_exempt_logins_json").notNull().default("[]"), // Force-rebase-before-merge window in minutes (#2552): null = never force (default). Enforcement lands in // runAgentMaintenancePlanAndExecute, not here. requireFreshRebaseWindowMinutes: integer("require_fresh_rebase_window_minutes"), - // Account-age throttle (#2561, anti-abuse): null = off (default). Enforcement lands in - // runAgentMaintenancePlanAndExecute, not here. - accountAgeThresholdDays: integer("account_age_threshold_days"), - newAccountLabel: text("new_account_label").notNull().default("new-account"), - // Per-command @loopover rate limit (#2560, anti-abuse): generalizes review-nag's cooldown pattern to every - // command, keyed by (actor, command, targetKey) independent of review-nag's own thread-author-only scope. - commandRateLimitPolicy: text("command_rate_limit_policy").notNull().default("off"), - commandRateLimitMaxPerWindow: integer("command_rate_limit_max_per_window").notNull().default(20), - commandRateLimitAiMaxPerWindow: integer("command_rate_limit_ai_max_per_window").notNull().default(5), - commandRateLimitWindowHours: integer("command_rate_limit_window_hours").notNull().default(24), // Draft-PR close policy (#draft-pr-close-policy): off by default -- enforces on ANY draft (including the // first one, before a review has run), so a maintainer opts in deliberately rather than getting it on by // default. diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 26340d97e2..b656733b19 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -2635,8 +2635,8 @@ describe("api routes", () => { // #4618/#5373: gateCheckMode is an unknown key with no effect here (removed from RepositorySettings // entirely) -- included to confirm it is silently ignored, not to drive reviewCheckMode. // mergeTrainMode moved off the DB entirely (Batch B, loopover#6443) -- no longer a writable key on this - // route, config-as-code only via .loopover.yml now. - body: JSON.stringify({ gateCheckMode: "enabled", reviewCheckMode: "required", slopGateMode: "block", slopGateMinScore: 55, qualityGateMode: "block", autonomy: { merge: "auto_with_approval", deploy: "auto" }, autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" }, agentPaused: true, agentDryRun: true }), + // route, config-as-code only via .loopover.yml now. Same for autoMaintain (loopover#6445). + body: JSON.stringify({ gateCheckMode: "enabled", reviewCheckMode: "required", slopGateMode: "block", slopGateMinScore: 55, qualityGateMode: "block", autonomy: { merge: "auto_with_approval", deploy: "auto" }, agentPaused: true, agentDryRun: true }), }, ownerEnv, ); @@ -2647,7 +2647,6 @@ describe("api routes", () => { slopGateMinScore: 55, qualityGateMode: "advisory", // #2267: downgraded, not persisted as "block" autonomy: { merge: "auto_with_approval" }, // unknown action class dropped by the DB normalizer - autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" }, agentPaused: true, // #776 kill-switch agentDryRun: true, }); @@ -2660,13 +2659,8 @@ describe("api routes", () => { ); expect(settingsUpdateOff.status).toBe(200); await expect(settingsUpdateOff.json()).resolves.toMatchObject({ reviewCheckMode: "required" }); - // requireApprovals is bounded at the API boundary — an out-of-range value is rejected, not silently clamped. - const settingsBadApprovals = await app.request( - "/v1/repos/repo-owner/owned-repo/settings", - { method: "PUT", headers: ownerHeaders, body: JSON.stringify({ autoMaintain: { requireApprovals: 99 } }) }, - ownerEnv, - ); - expect(settingsBadApprovals.status).toBe(400); + // autoMaintain moved off the DB entirely (config-as-code, loopover#6445) -- no longer a writable key on + // this route, so it's no longer validated at this API boundary either. const settingsInvalid = await app.request( "/v1/repos/repo-owner/owned-repo/settings", { method: "PUT", headers: ownerHeaders, body: JSON.stringify({ reviewCheckMode: "nonsense" }) }, diff --git a/test/integration/maintainer-activation.test.ts b/test/integration/maintainer-activation.test.ts index 35c2c19979..1d9adbfa8e 100644 --- a/test/integration/maintainer-activation.test.ts +++ b/test/integration/maintainer-activation.test.ts @@ -143,11 +143,13 @@ describe("maintainer activation routes", () => { const response = await app.request(`${PATH_ACTIVATE.replace("/activation", "/settings")}`, { method: "PUT", headers: { cookie: `loopover_session=${token}`, "content-type": "application/json" }, - body: JSON.stringify({ autonomy: { merge: "auto_with_approval" }, autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" } }), + // autoMaintain moved off the DB entirely (config-as-code, loopover#6445) -- no longer a writable key on + // this route. + body: JSON.stringify({ autonomy: { merge: "auto_with_approval" } }), }, env); expect(response.status).toBe(200); - expect(await response.json()).toMatchObject({ autonomy: { merge: "auto_with_approval" }, autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" } }); + expect(await response.json()).toMatchObject({ autonomy: { merge: "auto_with_approval" } }); }); it("persists selfAuthoredLinkedIssueGateMode from the settings PUT (API/OpenAPI parity)", async () => { diff --git a/test/unit/agent-approval-queue.test.ts b/test/unit/agent-approval-queue.test.ts index b37ac63866..8b9d0845ad 100644 --- a/test/unit/agent-approval-queue.test.ts +++ b/test/unit/agent-approval-queue.test.ts @@ -640,7 +640,9 @@ describe("agent approval queue (#779)", () => { it("accept re-syncs the merge method to the CURRENT repo config, not the staging-time snapshot (#2131)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: "x" }); // Staged while the default was "squash"; the maintainer has since changed the repo's default to "merge". - await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: { merge: "auto_with_approval" }, autoMaintain: { mergeMethod: "merge", requireApprovals: 0 } }); + // autoMaintain moved off the DB entirely (config-as-code, loopover#6445) -- set via manifest injection. + await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: { merge: "auto_with_approval" } }); + await upsertRepoFocusManifest(env, "owner/repo", { settings: { autoMaintain: { mergeMethod: "merge", requireApprovals: 0 } } }); await seedInstallation(env); await upsertPullRequestFromGitHub(env, "owner/repo", { number: 7, title: "PR", state: "open", user: { login: "contributor" }, head: { sha: "h7" }, labels: [], body: "x" }); const { action } = await createPendingAgentActionIfAbsent(env, { repoFullName: "owner/repo", pullNumber: 7, installationId: 5, actionClass: "merge", autonomyLevel: "auto_with_approval", params: { mergeMethod: "squash", expectedHeadSha: "h7" }, reason: "clean" }); diff --git a/test/unit/data-spine.test.ts b/test/unit/data-spine.test.ts index 7090cdf6e7..7b856b46c1 100644 --- a/test/unit/data-spine.test.ts +++ b/test/unit/data-spine.test.ts @@ -45,6 +45,8 @@ import { upsertRepositoryFromGitHub, upsertRepositorySettings, } from "../../src/db/repositories"; +import { resolveRepositorySettings } from "../../src/settings/repository-settings"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; import { createTestEnv } from "../helpers/d1"; describe("data spine repositories", () => { @@ -297,11 +299,13 @@ describe("data spine repositories", () => { await upsertRepositorySettings(env, { repoFullName: "owner/autonomyrepo", autonomy: { merge: "observe" } }); expect((await getRepositorySettings(env, "owner/autonomyrepo")).autonomy).toEqual({ merge: "observe" }); // update persists expect((await getRepositorySettings(env, "owner/defaultpack")).autonomy).toEqual({}); // deny-by-default - // #774 autoMaintain round-trips, clamps requireApprovals, and defaults to squash/1. + // #774/loopover#6445: autoMaintain moved off the DB entirely (config-as-code only via .loopover.yml's + // settings: block now) -- getRepositorySettings always resolves the built-in squash/1 default, + // ignoring any caller-supplied override. await upsertRepositorySettings(env, { repoFullName: "owner/automaintainrepo", autoMaintain: { requireApprovals: 99, mergeMethod: "rebase" } }); - expect((await getRepositorySettings(env, "owner/automaintainrepo")).autoMaintain).toEqual({ requireApprovals: 10, mergeMethod: "rebase" }); + expect((await getRepositorySettings(env, "owner/automaintainrepo")).autoMaintain).toEqual({ requireApprovals: 1, mergeMethod: "squash" }); await upsertRepositorySettings(env, { repoFullName: "owner/automaintainrepo", autoMaintain: { requireApprovals: 0, mergeMethod: "merge" } }); - expect((await getRepositorySettings(env, "owner/automaintainrepo")).autoMaintain).toEqual({ requireApprovals: 0, mergeMethod: "merge" }); // update persists + expect((await getRepositorySettings(env, "owner/automaintainrepo")).autoMaintain).toEqual({ requireApprovals: 1, mergeMethod: "squash" }); expect((await getRepositorySettings(env, "owner/defaultpack")).autoMaintain).toEqual({ requireApprovals: 1, mergeMethod: "squash" }); // defaults // #776 kill-switch + dry-run round-trip (insert + update) and default false. await upsertRepositorySettings(env, { repoFullName: "owner/saferepo", agentPaused: true, agentDryRun: true }); @@ -309,30 +313,31 @@ describe("data spine repositories", () => { await upsertRepositorySettings(env, { repoFullName: "owner/saferepo", agentPaused: false }); expect((await getRepositorySettings(env, "owner/saferepo")).agentPaused).toBe(false); // update persists expect(await getRepositorySettings(env, "owner/defaultpack")).toMatchObject({ agentPaused: false, agentDryRun: false }); // defaults - // #2270 per-contributor open PR/issue caps: no row and no cap set both default to null (disabled). + // #2270/loopover#6445: per-contributor open PR/issue caps moved off the DB entirely -- config-as-code + // only via .loopover.yml's settings: block now. getRepositorySettings always resolves the conservative + // null (disabled) default, ignoring any caller-supplied override; no row and no cap set both default to + // null (disabled) too. expect(await getRepositorySettings(env, "missing/repo")).toMatchObject({ contributorOpenPrCap: null, contributorOpenIssueCap: null }); expect(await getRepositorySettings(env, "owner/defaultpack")).toMatchObject({ contributorOpenPrCap: null, contributorOpenIssueCap: null }); - // Round-trips on insert and persists on update. await upsertRepositorySettings(env, { repoFullName: "owner/caprepo", contributorOpenPrCap: 2, contributorOpenIssueCap: 5 }); - expect(await getRepositorySettings(env, "owner/caprepo")).toMatchObject({ contributorOpenPrCap: 2, contributorOpenIssueCap: 5 }); - await upsertRepositorySettings(env, { repoFullName: "owner/caprepo", contributorOpenPrCap: 3, contributorOpenIssueCap: null }); - expect(await getRepositorySettings(env, "owner/caprepo")).toMatchObject({ contributorOpenPrCap: 3, contributorOpenIssueCap: null }); // update persists + can clear - await upsertRepositorySettings(env, { repoFullName: "owner/caprepo", contributorOpenPrCap: 101, contributorOpenIssueCap: 150 }); - expect(await getRepositorySettings(env, "owner/caprepo")).toMatchObject({ contributorOpenPrCap: 100, contributorOpenIssueCap: 100 }); // clamps to the live-check sample budget - // A cap must be a positive whole number: fractional, non-positive, and non-finite values are all - // dropped to null rather than silently coerced (there's no such thing as "allow 2.5 open PRs"). - await upsertRepositorySettings(env, { repoFullName: "owner/badcaprepo", contributorOpenPrCap: 2.5 as never }); - expect((await getRepositorySettings(env, "owner/badcaprepo")).contributorOpenPrCap).toBeNull(); - await upsertRepositorySettings(env, { repoFullName: "owner/badcaprepo", contributorOpenPrCap: 0 }); - expect((await getRepositorySettings(env, "owner/badcaprepo")).contributorOpenPrCap).toBeNull(); - await upsertRepositorySettings(env, { repoFullName: "owner/badcaprepo", contributorOpenPrCap: Number.NaN as never }); - expect((await getRepositorySettings(env, "owner/badcaprepo")).contributorOpenPrCap).toBeNull(); - // contributorCapLabel (#2270) round-trips and defaults to "over-contributor-limit". + expect(await getRepositorySettings(env, "owner/caprepo")).toMatchObject({ contributorOpenPrCap: null, contributorOpenIssueCap: null }); + // resolveRepositorySettings honors an explicit manifest override, including the same validation the DB + // layer used to enforce: a cap must be a positive whole number (fractional/non-positive/non-finite all + // drop to null -- there's no such thing as "allow 2.5 open PRs"), clamped to the live-verification + // sample budget of 100. + await upsertRepoFocusManifest(env, "owner/caprepo", { settings: { contributorOpenPrCap: 3, contributorOpenIssueCap: 150 } }); + expect(await resolveRepositorySettings(env, "owner/caprepo")).toMatchObject({ contributorOpenPrCap: 3, contributorOpenIssueCap: 100 }); + await upsertRepoFocusManifest(env, "owner/badcaprepo", { settings: { contributorOpenPrCap: 2.5 as never } }); + expect((await resolveRepositorySettings(env, "owner/badcaprepo")).contributorOpenPrCap).toBeNull(); + await upsertRepoFocusManifest(env, "owner/badcaprepo", { settings: { contributorOpenPrCap: 0 } }); + expect((await resolveRepositorySettings(env, "owner/badcaprepo")).contributorOpenPrCap).toBeNull(); + // contributorCapLabel (#2270/loopover#6445): moved off the DB entirely too -- defaults to + // "over-contributor-limit", ignores a caller-supplied DB override, honors a manifest override. expect((await getRepositorySettings(env, "missing/repo")).contributorCapLabel).toBe("over-contributor-limit"); await upsertRepositorySettings(env, { repoFullName: "owner/caprepo", contributorCapLabel: "spam-cap" }); - expect((await getRepositorySettings(env, "owner/caprepo")).contributorCapLabel).toBe("spam-cap"); - await upsertRepositorySettings(env, { repoFullName: "owner/caprepo", contributorCapLabel: "renamed-cap" }); - expect((await getRepositorySettings(env, "owner/caprepo")).contributorCapLabel).toBe("renamed-cap"); // update persists + expect((await getRepositorySettings(env, "owner/caprepo")).contributorCapLabel).toBe("over-contributor-limit"); + await upsertRepoFocusManifest(env, "owner/caprepo", { settings: { contributorCapLabel: "renamed-cap" } }); + expect((await resolveRepositorySettings(env, "owner/caprepo")).contributorCapLabel).toBe("renamed-cap"); // #2552 force-rebase-before-merge window: no row and no override both default to null (never force). expect((await getRepositorySettings(env, "missing/repo")).requireFreshRebaseWindowMinutes).toBeNull(); expect((await getRepositorySettings(env, "owner/defaultpack")).requireFreshRebaseWindowMinutes).toBeNull(); @@ -347,13 +352,17 @@ describe("data spine repositories", () => { // contributorOpenPrCap above -- it must NOT inherit that unrelated cap's 100 ceiling. await upsertRepositorySettings(env, { repoFullName: "owner/rebasewindowrepo", requireFreshRebaseWindowMinutes: 500 }); expect((await getRepositorySettings(env, "owner/rebasewindowrepo")).requireFreshRebaseWindowMinutes).toBe(500); - // #1936 minimum account-age gate: no row and no override both default to null (never enforced); round-trips, - // and (REGRESSION, same reasoning as requireFreshRebaseWindowMinutes above) is not capped at 100. + // #1936/loopover#6445: minimum account-age gate moved off the DB entirely -- config-as-code only via + // .loopover.yml's settings: block now. No row and no override both default to null (never enforced); + // a caller-supplied DB override is ignored; resolveRepositorySettings honors a manifest override + // (still not capped at 100, unlike the live-verification-sample-bounded caps above). expect((await getRepositorySettings(env, "missing/repo")).accountAgeThresholdDays).toBeNull(); await upsertRepositorySettings(env, { repoFullName: "owner/accountagerepo", accountAgeThresholdDays: 365 }); - expect((await getRepositorySettings(env, "owner/accountagerepo")).accountAgeThresholdDays).toBe(365); - // #2463 review-nag cooldown + shared exemption list: no row and no override both default to off/3/5/the - // default label/empty exemption list. + expect((await getRepositorySettings(env, "owner/accountagerepo")).accountAgeThresholdDays).toBeNull(); + await upsertRepoFocusManifest(env, "owner/accountagerepo", { settings: { accountAgeThresholdDays: 365 } }); + expect((await resolveRepositorySettings(env, "owner/accountagerepo")).accountAgeThresholdDays).toBe(365); + // #2463/loopover#6445: review-nag cooldown + shared exemption list moved off the DB entirely too. No row + // and no override both default to off/3/5/the default label/empty exemption list. expect(await getRepositorySettings(env, "missing/repo")).toMatchObject({ reviewNagPolicy: "off", reviewNagMaxPings: 3, @@ -362,7 +371,7 @@ describe("data spine repositories", () => { autoCloseExemptLogins: [], }); expect(await getRepositorySettings(env, "owner/defaultpack")).toMatchObject({ reviewNagPolicy: "off", autoCloseExemptLogins: [] }); - // Round-trips on insert and persists on update. + // A caller-supplied DB override is silently ignored -- always the built-in defaults. await upsertRepositorySettings(env, { repoFullName: "owner/nagrepo", reviewNagPolicy: "close", @@ -372,22 +381,42 @@ describe("data spine repositories", () => { autoCloseExemptLogins: ["Trusted-Regular"], }); expect(await getRepositorySettings(env, "owner/nagrepo")).toMatchObject({ + reviewNagPolicy: "off", + reviewNagMaxPings: 3, + reviewNagCooldownDays: 5, + reviewNagLabel: "review-nag-cooldown", + autoCloseExemptLogins: [], + }); + // resolveRepositorySettings honors an explicit manifest override, round-tripping through a re-upsert + // that carries it forward explicitly (upsertRepoFocusManifest replaces the manifest wholesale). + await upsertRepoFocusManifest(env, "owner/nagrepo", { + settings: { + reviewNagPolicy: "close", + reviewNagMaxPings: 5, + reviewNagCooldownDays: 10, + reviewNagLabel: "too-many-pings", + autoCloseExemptLogins: ["Trusted-Regular"], + }, + }); + expect(await resolveRepositorySettings(env, "owner/nagrepo")).toMatchObject({ reviewNagPolicy: "close", reviewNagMaxPings: 5, reviewNagCooldownDays: 10, reviewNagLabel: "too-many-pings", autoCloseExemptLogins: ["Trusted-Regular"], }); - await upsertRepositorySettings(env, { repoFullName: "owner/nagrepo", reviewNagPolicy: "hold", autoCloseExemptLogins: [] }); - expect(await getRepositorySettings(env, "owner/nagrepo")).toMatchObject({ reviewNagPolicy: "hold", autoCloseExemptLogins: [] }); // update persists + can clear + await upsertRepoFocusManifest(env, "owner/nagrepo", { settings: { reviewNagPolicy: "hold", autoCloseExemptLogins: [] } }); + expect(await resolveRepositorySettings(env, "owner/nagrepo")).toMatchObject({ reviewNagPolicy: "hold", autoCloseExemptLogins: [] }); // update persists + can clear // An invalid policy string is dropped to "off"; a non-positive/fractional ping count or cooldown falls // back to its default rather than being silently coerced. - await upsertRepositorySettings(env, { repoFullName: "owner/badnagrepo", reviewNagPolicy: "delete-everything" as never, reviewNagMaxPings: -1, reviewNagCooldownDays: 2.5 as never }); - expect(await getRepositorySettings(env, "owner/badnagrepo")).toMatchObject({ reviewNagPolicy: "off", reviewNagMaxPings: 3, reviewNagCooldownDays: 5 }); - await upsertRepositorySettings(env, { repoFullName: "owner/bigwindowrepo", reviewNagMaxPings: 1_000, reviewNagCooldownDays: 1_000_000_000 }); - expect(await getRepositorySettings(env, "owner/bigwindowrepo")).toMatchObject({ reviewNagMaxPings: 1_000, reviewNagCooldownDays: 365 }); - await env.DB.prepare("update repository_settings set review_nag_cooldown_days = ? where repo_full_name = ?").bind(1_000_000_000, "owner/bigwindowrepo").run(); - expect(await getRepositorySettings(env, "owner/bigwindowrepo")).toMatchObject({ reviewNagMaxPings: 1_000, reviewNagCooldownDays: 365 }); + await upsertRepoFocusManifest(env, "owner/badnagrepo", { settings: { reviewNagPolicy: "delete-everything" as never, reviewNagMaxPings: -1, reviewNagCooldownDays: 2.5 as never } }); + expect(await resolveRepositorySettings(env, "owner/badnagrepo")).toMatchObject({ reviewNagPolicy: "off", reviewNagMaxPings: 3, reviewNagCooldownDays: 5 }); + // REGRESSION: a cooldown ABOVE the 365-day ceiling is rejected outright (falls back to the built-in + // default), unlike the old DB-layer behavior which clamped it down to 365 -- the manifest parser's + // normalizeOptionalPositiveInteger + explicit ceiling check drops an out-of-range value rather than + // silently coercing it. + await upsertRepoFocusManifest(env, "owner/bigwindowrepo", { settings: { reviewNagMaxPings: 1_000, reviewNagCooldownDays: 1_000_000_000 } }); + expect(await resolveRepositorySettings(env, "owner/bigwindowrepo")).toMatchObject({ reviewNagMaxPings: 1_000, reviewNagCooldownDays: 5 }); expect(updated.slopAiAdvisory).toBe(false); expect(await getRepoSyncState(env, "missing/repo")).toBeNull(); expect(await getPullRequest(env, "owner/repo", 404)).toBeNull(); diff --git a/test/unit/queue-3.test.ts b/test/unit/queue-3.test.ts index 22a3fd6a60..4480a58f56 100644 --- a/test/unit/queue-3.test.ts +++ b/test/unit/queue-3.test.ts @@ -2550,11 +2550,10 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: opts.autonomy ?? { merge: "auto", update_branch: "auto", label: "auto" }, - autoMaintain: { requireApprovals: 0, mergeMethod: "squash" }, gatePack: "oss-anti-slop", ...(opts.requireFreshRebaseWindowMinutes !== undefined ? { requireFreshRebaseWindowMinutes: opts.requireFreshRebaseWindowMinutes } : {}), }); - await upsertRepoFocusManifest(env, "owner/repo", { settings: { checkRunMode: "off", commentMode: "off", publicSurface: "off", aiReviewMode: "off", reviewCheckMode: "required" } }); + await upsertRepoFocusManifest(env, "owner/repo", { settings: { checkRunMode: "off", commentMode: "off", publicSurface: "off", aiReviewMode: "off", reviewCheckMode: "required", autoMaintain: { requireApprovals: 0, mergeMethod: "squash" } } }); await upsertPullRequestFromGitHub(env, "owner/repo", { number: prNumber, title: "Fresh rebase PR", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main" }, labels: [], body: "" }); } @@ -2706,10 +2705,9 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, // The label is the configurable `.loopover.yml` value below — nothing is hard-coded. }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", contributorCapLabel: "spam-cap", reviewCheckMode: "required", aiReviewMode: "advisory" } }, "repo_file"); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", contributorCapLabel: "spam-cap", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }, "repo_file"); const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -2771,10 +2769,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - autoCloseExemptLogins: ["sentry[bot]"], }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2, autoCloseExemptLogins: ["sentry[bot]"] } }); const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -2853,10 +2849,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: true, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2, contributorCapCancelCi: true } }); const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [101], queued: [102] })); @@ -2890,10 +2884,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: true, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2, contributorCapCancelCi: true } }); const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [103] })); const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; @@ -2930,10 +2922,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: true, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2, contributorCapCancelCi: true } }); const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; vi.stubGlobal( "fetch", @@ -2973,10 +2963,9 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, // contributorCapCancelCi intentionally omitted — off by default, no CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT set. }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [201] })); @@ -3008,10 +2997,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: true, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2, contributorCapCancelCi: true } }); const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -3052,10 +3039,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: true, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2, contributorCapCancelCi: true } }); const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; vi.stubGlobal( "fetch", @@ -3092,10 +3077,9 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, // contributorCapCancelCi intentionally omitted (null) -- falls back to the env var default above. }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [301] })); @@ -3126,10 +3110,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: false, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2, contributorCapCancelCi: false } }); const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [401] })); @@ -3163,9 +3145,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 1, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 1 } }); const seen = { closed: false, comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -3217,9 +3198,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 100, // above SIBLING_COUNT + 1 — this test only cares about concurrency, not closing. }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "off", publicSurface: "off", checkRunMode: "off", reviewCheckMode: "required" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "off", publicSurface: "off", checkRunMode: "off", reviewCheckMode: "required", contributorOpenPrCap: 100 } }); let inFlight = 0; let maxInFlight = 0; const siblingCheckPattern = new RegExp(`/pulls/(?:${Array.from({ length: SIBLING_COUNT }, (_, i) => i + 1).join("|")})$`); @@ -3327,9 +3307,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -3561,9 +3540,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" }, - autoCloseExemptLogins: ["farmer99"], }); - await upsertRepoFocusManifest(env, "JSONbored/repo-a", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/repo-a", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", autoCloseExemptLogins: ["farmer99"] } }); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -3730,9 +3708,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -3782,9 +3759,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -3862,10 +3838,8 @@ describe("queue processors", () => { repoFullName: "JSONbored/gittensory", // #label-scoping: the cap label/close rides on `close`; the new-account label rides on `review_state_label`. autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenPrCap: 4, - accountAgeThresholdDays: 30, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required", contributorOpenPrCap: 4, accountAgeThresholdDays: 30 } }); const seen = { labels: [] as string[], closed: false }; // Account created 2 days ago — well under the 30-day threshold. vi.stubGlobal("fetch", stubAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); @@ -3897,10 +3871,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenPrCap: 2, - accountAgeThresholdDays: 30, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required", contributorOpenPrCap: 2, accountAgeThresholdDays: 30 } }); const seen = { labels: [] as string[], closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -3950,10 +3922,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 4, - accountAgeThresholdDays: 30, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required", contributorOpenPrCap: 4, accountAgeThresholdDays: 30 } }); const seen = { labels: [] as string[], closed: false }; // Account created 2 years ago — well over the 30-day threshold. vi.stubGlobal("fetch", stubAccountAgeFetch(65, new Date(Date.now() - 730 * 24 * 60 * 60 * 1000).toISOString(), seen)); @@ -3983,9 +3953,9 @@ describe("queue processors", () => { }); await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", - accountAgeThresholdDays: 30, + reviewCheckMode: "required", }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required", accountAgeThresholdDays: 30 } }); const seen = { labels: [] as string[], closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -4083,10 +4053,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, - newAccountLabel: "custom-new-account-label", }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required", accountAgeThresholdDays: 30, newAccountLabel: "custom-new-account-label" } }); const seen = { labels: [] as string[], closed: false }; vi.stubGlobal("fetch", stubAccountAgeFetch(68, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); @@ -4115,9 +4083,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", // autonomy intentionally omitted — deny-by-default ("observe" for every action class, including "review_state_label"). - accountAgeThresholdDays: 30, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", reviewCheckMode: "required", accountAgeThresholdDays: 30 } }); const seen = { labels: [] as string[], closed: false }; vi.stubGlobal("fetch", stubAccountAgeFetch(69, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); @@ -4169,9 +4136,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenIssueCap: 4, - accountAgeThresholdDays: 30, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { 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)); @@ -4202,8 +4168,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenIssueCap: 4, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 4 } }); let accountAgeUsersFetched = false; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -4242,9 +4208,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenIssueCap: 4, - accountAgeThresholdDays: 30, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { 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)); @@ -4275,9 +4240,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto" }, - contributorOpenIssueCap: 4, - accountAgeThresholdDays: 30, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { 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)); @@ -4308,9 +4272,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenIssueCap: 4, - accountAgeThresholdDays: 30, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 4, accountAgeThresholdDays: 30 } }); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -4347,9 +4310,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, - newAccountLabel: "custom-new-account-label", }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { 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)); @@ -4378,8 +4340,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { accountAgeThresholdDays: 30 } }); const seen = { labels: [] as string[], closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -4421,8 +4383,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { accountAgeThresholdDays: 30 } }); const seen = { labels: [] as string[], closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -4461,8 +4423,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { accountAgeThresholdDays: 30 } }); const seen = { labels: [] as string[], closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -4508,9 +4470,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const closedNumbers = new Set(); const fanned: import("../../src/types").JobMessage[] = []; const realSend = env.JOBS.send.bind(env.JOBS); @@ -4591,9 +4552,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const fanned: import("../../src/types").JobMessage[] = []; const realSend = env.JOBS.send.bind(env.JOBS); env.JOBS.send = (async (message: import("../../src/types").JobMessage, options?: QueueSendOptions) => { @@ -4644,9 +4604,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); env.JOBS.send = (async () => { throw new Error("queue send boom"); }) as typeof env.JOBS.send; @@ -4701,9 +4660,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const fanned: import("../../src/types").JobMessage[] = []; const realSend = env.JOBS.send.bind(env.JOBS); env.JOBS.send = (async (message: import("../../src/types").JobMessage, options?: QueueSendOptions) => { @@ -4777,9 +4735,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commentMode: "all_prs", publicSurface: "comment_only", checkRunMode: "off", reviewCheckMode: "required", aiReviewMode: "advisory", contributorOpenPrCap: 2 } }); const realGetPullRequest = repositoriesModule.getPullRequest; vi.spyOn(repositoriesModule, "getPullRequest").mockImplementation(async (spyEnv, fullName, number) => { if (number === 56) return null; // simulate PR56 not being found locally at wake time @@ -4838,9 +4795,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenIssueCap: 2, }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorCapLabel: "spam-cap" } }, "repo_file"); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorCapLabel: "spam-cap", contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -4891,8 +4847,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenIssueCap: SIBLING_COUNT, }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: SIBLING_COUNT } }, "repo_file"); let inFlight = 0; let maxInFlight = 0; let closed = false; @@ -4941,9 +4897,8 @@ describe("queue processors", () => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, - contributorOpenIssueCap: 2, - autoCloseExemptLogins: ["sentry[bot]"], }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2, autoCloseExemptLogins: ["sentry[bot]"] } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -4984,7 +4939,8 @@ describe("queue processors", () => { }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Farmer issue one (stale-open)", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Farmer issue two", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5027,7 +4983,8 @@ describe("queue processors", () => { }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Farmer issue one", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Farmer issue two", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5066,8 +5023,8 @@ describe("queue processors", () => { }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Farmer issue one", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Farmer issue two", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorCapLabel: "spam-cap" } }, "repo_file"); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorCapLabel: "spam-cap", contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5104,7 +5061,8 @@ describe("queue processors", () => { }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Farmer issue one (stale-open)", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Farmer issue two", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false, sawPublicToken: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5348,7 +5306,8 @@ describe("queue processors", () => { }); await upsertIssueFromGitHub(env, "JSONbored/repo-a", { number: 60, title: "Farmer issue one", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "JSONbored/repo-a", { number: 61, title: "Farmer issue two", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/repo-a", { settings: { contributorOpenIssueCap: 2 } }); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5390,7 +5349,8 @@ describe("queue processors", () => { }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Farmer issue one", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Farmer issue two", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5472,7 +5432,8 @@ describe("queue processors", () => { }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Owner issue one", state: "open", user: { login: "JSONbored" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Owner issue two", state: "open", user: { login: "JSONbored" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5505,7 +5466,8 @@ describe("queue processors", () => { }); // Only ONE pre-existing open issue from this author — the incoming issue is their 2nd, exactly at the cap. await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Farmer issue one", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5541,7 +5503,8 @@ describe("queue processors", () => { // No acting autonomy for label/close — deny-by-default (autonomy: {}) means planAgentMaintenanceActions // plans nothing at all, so this exercises the "planned.length === 0" early return distinctly from the // disabled-cap case above (here the cap DOES match; there is simply nothing to execute). - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: {}, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: {} }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); @@ -5583,7 +5546,8 @@ describe("queue processors", () => { }); await upsertIssueFromGitHub(env, "noslash", { number: 60, title: "Farmer issue one", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "noslash", { number: 61, title: "Farmer issue two", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "noslash", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "noslash", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "noslash", { settings: { contributorOpenIssueCap: 2 } }); vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { const url = input.toString(); if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); @@ -5620,7 +5584,8 @@ describe("queue processors", () => { await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 59, title: "Ghost issue", state: "open", labels: [], body: "z" }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Farmer issue one", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Farmer issue two", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" }, contributorOpenIssueCap: 2 }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorOpenIssueCap: 2 } }, "repo_file"); const seen = { closed: false }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); diff --git a/test/unit/queue-5.test.ts b/test/unit/queue-5.test.ts index 3413f4d58d..1fdbe35dc5 100644 --- a/test/unit/queue-5.test.ts +++ b/test/unit/queue-5.test.ts @@ -303,7 +303,7 @@ describe("queue processors", () => { // getRepositorySettings neutralizes it first. Mock resolveRepositorySettings directly so this test proves // processors.ts's OWN Math.min(reviewNagCooldownDays, MAX_REVIEW_NAG_COOLDOWN_DAYS) guard, not the DB layer. const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "hold", reviewNagMaxPings: 3 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "hold", reviewNagMaxPings: 3 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 206, title: "Huge cooldown", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); // Three prior pings, all 400 DAYS ago -- outside the 365-day cap, but well within an uncapped // "1,000,000,000-day" window. If the guard clamps correctly, these fall outside the window and don't @@ -346,7 +346,7 @@ describe("queue processors", () => { it("records pings under the configured threshold without acting; the normal @loopover reply still proceeds", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 201, title: "Under threshold", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubReviewNagFetch(201, seen); @@ -375,7 +375,7 @@ describe("queue processors", () => { it("hold policy: posts a cooldown reply and short-circuits once the threshold is crossed", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "hold", reviewNagMaxPings: 3, reviewNagCooldownDays: 5 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "hold", reviewNagMaxPings: 3, reviewNagCooldownDays: 5 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 202, title: "Hold cooldown", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#202", outcome: "completed" }); @@ -409,8 +409,8 @@ describe("queue processors", () => { 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: ["issue_comment"] }, repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, autonomy: { close: "auto", label: "auto" } }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagLabel: "too-chatty" } }, "repo_file"); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagLabel: "too-chatty" } }, "repo_file"); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 203, title: "Close cooldown", state: "open", user: { login: "chatty" }, head: { sha: "sha203" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#203", outcome: "completed" }); @@ -442,7 +442,8 @@ describe("queue processors", () => { 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: ["issue_comment"] }, repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, autonomy: { close: "auto", label: "auto" } }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } }); // PR A: "chatty" already sent 3 pings (the full budget) and PR A was closed for it -- this is the exact // state left behind by the "close policy on a PR thread" scenario above. await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 220, title: "PR A (already closed)", state: "closed", user: { login: "chatty" }, head: { sha: "sha220" }, author_association: "NONE", labels: [], body: "" }); @@ -479,7 +480,7 @@ describe("queue processors", () => { it("close policy degrades to hold on an ISSUE thread (no closeIssue primitive yet)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#204", outcome: "completed" }); } @@ -503,7 +504,7 @@ describe("queue processors", () => { it("never throttles an exempt login, even over threshold", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, autoCloseExemptLogins: ["chatty"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3, autoCloseExemptLogins: ["chatty"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 205, title: "Exempt author", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 5; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#205", outcome: "completed" }); @@ -529,7 +530,7 @@ describe("queue processors", () => { it("never throttles a third party pinging on someone else's PR — only the thread's OWN author is tracked", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 206, title: "Third party pinger", state: "open", user: { login: "pr-author" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubReviewNagFetch(206, seen); @@ -558,7 +559,7 @@ describe("queue processors", () => { installation: { id: 123, account: { login: "", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["issue_comment"] }, repositories: [{ name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }], }); - await upsertRepositorySettings(env, { repoFullName: "noslash", reviewNagPolicy: "hold", reviewNagMaxPings: 3 }); + await upsertRepoFocusManifest(env, "noslash", { settings: { reviewNagPolicy: "hold", reviewNagMaxPings: 3 } }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "noslash#209", outcome: "completed" }); } @@ -586,7 +587,7 @@ describe("queue processors", () => { it("never throttles the literal repo owner self-pinging their own PR", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 1 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 1 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 207, title: "Owner PR", state: "open", user: { login: "JSONbored" }, author_association: "OWNER", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubReviewNagFetch(207, seen); @@ -611,7 +612,7 @@ describe("queue processors", () => { it("never throttles an ADMIN_GITHUB_LOGINS fleet-operator, even over threshold", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), ADMIN_GITHUB_LOGINS: "fleet-admin" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 208, title: "Admin PR", state: "open", user: { login: "fleet-admin" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 5; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "fleet-admin", targetKey: "JSONbored/gittensory#208", outcome: "completed" }); @@ -637,7 +638,8 @@ describe("queue processors", () => { it("hold policy respects agentDryRun — records a denied cooldown-applied audit and never posts the reply live (#2258 parity)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "hold", reviewNagMaxPings: 3, agentDryRun: true }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "hold", reviewNagMaxPings: 3 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 209, title: "Dry-run hold", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#209", outcome: "completed" }); @@ -663,7 +665,7 @@ describe("queue processors", () => { it("close policy falls through harmlessly when the PR is no longer open by the time the threshold fires", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 210, title: "Already closed", state: "closed", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#210", outcome: "completed" }); @@ -689,7 +691,8 @@ describe("queue processors", () => { it("close policy records a denied cooldown-applied audit when autonomy is not acting for label/close (empty plan)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, autonomy: {} }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: {} }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 211, title: "Observe-only autonomy", state: "open", user: { login: "chatty" }, head: { sha: "sha211" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#211", outcome: "completed" }); @@ -716,7 +719,8 @@ describe("queue processors", () => { it("close policy denies the mutation (never crashes) when no installation is on record — installationPermissions falls back to null", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, autonomy: { close: "auto", label: "auto" } }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 212, title: "No installation row", state: "open", user: { login: "chatty" }, head: { sha: "sha212" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#212", outcome: "completed" }); @@ -771,7 +775,7 @@ describe("queue processors", () => { it("is off by default (no monitored logins configured) — no ping is tracked", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close" }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close" } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 300, title: "No monitored logins", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(300, seen); @@ -793,7 +797,7 @@ describe("queue processors", () => { it("detects a mention of a configured maintainer login and records a ping under threshold without acting", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 301, title: "Under threshold", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(301, seen); @@ -816,7 +820,7 @@ describe("queue processors", () => { it("REGRESSION: matches bot-shaped monitored logins literally", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["dependabot[bot]"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["dependabot[bot]"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 312, title: "Bot mention", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(312, seen); @@ -838,7 +842,7 @@ describe("queue processors", () => { it("REGRESSION: does not treat bot-login metacharacters as a regex character class", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMonitoredMentions: ["dependabot[bot]"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMonitoredMentions: ["dependabot[bot]"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 313, title: "Bot false positive", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(313, seen); @@ -860,7 +864,7 @@ describe("queue processors", () => { it("case-insensitively matches a monitored login and ignores an unrelated mention", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMonitoredMentions: ["JSONbored"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMonitoredMentions: ["JSONbored"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 302, title: "Case + unrelated", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(302, seen); @@ -897,7 +901,7 @@ describe("queue processors", () => { it("counts a monitored-login mention independently of the @loopover ping counter", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 303, title: "Independent counters", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(303, seen); @@ -922,7 +926,7 @@ describe("queue processors", () => { it("hold policy: posts a cooldown reply naming the mentioned login and short-circuits once the threshold is crossed", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "hold", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "hold", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 304, title: "Hold on mention", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.monitored_mention_ping", actor: "chatty", targetKey: "JSONbored/gittensory#304#mention:jsonbored", outcome: "completed" }); @@ -952,7 +956,8 @@ describe("queue processors", () => { 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: ["issue_comment"] }, repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"], reviewNagLabel: "too-chatty", autonomy: { close: "auto" } }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"], reviewNagLabel: "too-chatty" } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 305, title: "Close on mention", state: "open", user: { login: "chatty" }, head: { sha: "sha305" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.monitored_mention_ping", actor: "chatty", targetKey: "JSONbored/gittensory#305#mention:jsonbored", outcome: "completed" }); @@ -982,7 +987,8 @@ describe("queue processors", () => { 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: ["issue_comment"] }, repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"], autonomy: { close: "auto" } }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored"] } }); // PR A: "chatty" already sent 3 pings mentioning @JSONbored (the full budget) and PR A was closed for it. await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 320, title: "PR A (already closed)", state: "closed", user: { login: "chatty" }, head: { sha: "sha320" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { @@ -1019,13 +1025,8 @@ describe("queue processors", () => { 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: ["issue_comment"] }, repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - reviewNagPolicy: "close", - reviewNagMaxPings: 3, - reviewNagMonitoredMentions: ["JSONbored", "other-maintainer"], - autonomy: { close: "auto" }, - }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto" } }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3, reviewNagMonitoredMentions: ["JSONbored", "other-maintainer"] } }); // PR A: "chatty" already exhausted the @JSONbored budget (3 pings) -- same seed as the carryover test above. await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 322, title: "PR A (JSONbored exhausted)", state: "closed", user: { login: "chatty" }, head: { sha: "sha322" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { @@ -1058,7 +1059,7 @@ describe("queue processors", () => { it("does NOT throttle the repo owner, an admin login, an automation bot, or an exempt login", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), ADMIN_GITHUB_LOGINS: "fleet-admin" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 1, reviewNagMonitoredMentions: ["JSONbored"], autoCloseExemptLogins: ["trusted-regular"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 1, reviewNagMonitoredMentions: ["JSONbored"], autoCloseExemptLogins: ["trusted-regular"] } }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(306, seen); for (const [commenter, prNumber] of [ @@ -1087,7 +1088,7 @@ describe("queue processors", () => { it("does NOT throttle a third party mentioning the login on someone else's thread (thread-author-only scope)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMonitoredMentions: ["JSONbored"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMonitoredMentions: ["JSONbored"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 310, title: "Third party", state: "open", user: { login: "thread-author" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(310, seen); @@ -1109,7 +1110,7 @@ describe("queue processors", () => { it("REGRESSION: a redelivered webhook (same deliveryId) does not double-count the ping", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", reviewNagPolicy: "close", reviewNagMaxPings: 5, reviewNagMonitoredMentions: ["JSONbored"] }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 5, reviewNagMonitoredMentions: ["JSONbored"] } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 311, title: "Redelivery", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[], labels: [] as string[], closed: false }; stubMonitoredMentionFetch(311, seen); @@ -1171,7 +1172,7 @@ describe("queue processors", () => { it("records invocations under the configured threshold without holding — the normal reply still proceeds", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 5, commandRateLimitWindowHours: 24 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 5, commandRateLimitWindowHours: 24 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 301, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; stubCommandRateLimitFetch(301, seen); @@ -1185,7 +1186,7 @@ describe("queue processors", () => { it("hold policy: posts a cooldown reply and short-circuits once a CHEAP command crosses its threshold", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 3, commandRateLimitWindowHours: 24 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 3, commandRateLimitWindowHours: 24 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 302, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 3; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.command_invocation", actor: "maintainer", targetKey: "JSONbored/gittensory#302#help", outcome: "completed" }); @@ -1204,7 +1205,7 @@ describe("queue processors", () => { it("an AI-cost-bearing command uses the TIGHTER commandRateLimitAiMaxPerWindow default, not the cheap-command limit", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // Cheap-command limit left generous (20, the default); only the AI limit is tight enough to trip here. - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 2, commandRateLimitWindowHours: 24 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 2, commandRateLimitWindowHours: 24 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 303, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); for (let i = 0; i < 2; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.command_invocation", actor: "maintainer", targetKey: "JSONbored/gittensory#303#next-action", outcome: "completed" }); @@ -1220,7 +1221,7 @@ describe("queue processors", () => { it("commands have INDEPENDENT counters — repeatedly invoking one command never throttles a DIFFERENT command", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 1, commandRateLimitWindowHours: 24 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 1, commandRateLimitWindowHours: 24 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 304, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); // Already at the "help" limit (1) — a further "help" invocation would be held. await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.command_invocation", actor: "maintainer", targetKey: "JSONbored/gittensory#304#help", outcome: "completed" }); @@ -1239,7 +1240,7 @@ describe("queue processors", () => { // actor who exhausted the limit on one thread could get a full new budget simply by invoking the SAME // command on a DIFFERENT issue/PR number — the exact bypass class #4021 already fixed for review-nag. const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 1, commandRateLimitWindowHours: 24 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 1, commandRateLimitWindowHours: 24 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 340, title: "Rate limit target A", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 341, title: "Rate limit target B (fresh)", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); // Already at the "help" limit (1) on issue #340. @@ -1257,7 +1258,8 @@ describe("queue processors", () => { it("dry-run mode: holds the command but never posts a live cooldown comment", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 1, commandRateLimitWindowHours: 24, agentDryRun: true }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 1, commandRateLimitWindowHours: 24 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 305, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.command_invocation", actor: "maintainer", targetKey: "JSONbored/gittensory#305#help", outcome: "completed" }); const seen = { comments: [] as string[] }; @@ -1273,7 +1275,7 @@ describe("queue processors", () => { // second delivery would increment the counter again for what is really ONE real invocation, and could // incorrectly cross the rate-limit threshold on a redelivery alone. const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 1, commandRateLimitWindowHours: 24 }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { commandRateLimitPolicy: "hold", commandRateLimitMaxPerWindow: 1, commandRateLimitWindowHours: 24 } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 306, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; stubCommandRateLimitFetch(306, seen); @@ -1628,7 +1630,6 @@ describe("queue processors", () => { GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: async () => ({ response: '{"command": "blockers"}' }) } as unknown as Ai, }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 5, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 309, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; // advisoryAiRouting is config-as-code only -- enable intentRouting the real way, through the repo's @@ -1637,7 +1638,7 @@ describe("queue processors", () => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 5\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); @@ -1661,14 +1662,13 @@ describe("queue processors", () => { GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: async () => ({ response: '{"command": "ask"}' }) } as unknown as Ai, }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 5, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 315, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 5\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); @@ -1710,14 +1710,13 @@ describe("queue processors", () => { GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: advisoryRun } as unknown as Ai, }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 5, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 316, title: "Unauthorized intent routing", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 5\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); @@ -1744,14 +1743,13 @@ describe("queue processors", () => { GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: async () => ({ response: '{"command": null}' }) } as unknown as Ai, }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 5, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 311, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 5\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); @@ -1775,14 +1773,13 @@ describe("queue processors", () => { GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: async () => ({ response: '{"command": "blockers"}' }) } as unknown as Ai, }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 5, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 312, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 5\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); @@ -1803,7 +1800,6 @@ describe("queue processors", () => { it("#4596: hold policy throttles intent-routing once the AI ceiling is crossed and skips the classifier (fails open to the existing did-you-mean hint)", async () => { const advisoryRun = vi.fn(async () => ({ response: '{"command": "blockers"}' })); const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: advisoryRun } as unknown as Ai }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 1, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 313, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); // Already at the ceiling (1) -- the next invocation must be held before it ever reaches the classifier. await repositoriesModule.recordAuditEvent(env, { @@ -1817,7 +1813,7 @@ describe("queue processors", () => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 1\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); @@ -1840,7 +1836,6 @@ describe("queue processors", () => { // classifier's own independent counter. const advisoryRun = vi.fn(async () => ({ response: '{"command": "blockers"}' })); const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: advisoryRun } as unknown as Ai }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 1, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 342, title: "Rate limit target A", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 343, title: "Rate limit target B (fresh)", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); // Already at the ceiling (1) on issue #342. @@ -1855,7 +1850,7 @@ describe("queue processors", () => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 1\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); @@ -1877,14 +1872,13 @@ describe("queue processors", () => { it("#4596: REGRESSION: a redelivered webhook does not re-classify or double-count the intent-routing invocation", async () => { const advisoryRun = vi.fn(async () => ({ response: '{"command": "blockers"}' })); const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: advisoryRun } as unknown as Ai }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 5, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 314, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); const seen = { comments: [] as string[] }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 5\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); @@ -1937,7 +1931,6 @@ describe("queue processors", () => { GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI_ADVISORY: { run: async () => ({ response: '{"command": "blockers"}' }) } as unknown as Ai, }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandRateLimitPolicy: "hold", commandRateLimitAiMaxPerWindow: 5, commandRateLimitWindowHours: 24 }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 319, title: "Rate limit target", state: "open", user: { login: "own-pr-miner" }, author_association: "NONE", labels: [], body: "" }); await upsertOfficialMinerDetection(env, "own-pr-miner", { status: "confirmed", snapshot: queueMinerSnapshot("own-pr-miner") }, 60_000); const seen = { comments: [] as string[] }; @@ -1945,7 +1938,7 @@ describe("queue processors", () => { const url = input.toString(); const method = init?.method ?? "GET"; if (url.includes("raw.githubusercontent.com") && url.includes(".loopover.yml")) { - return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n", { status: 200 }); + return new Response("settings:\n advisoryAiRouting:\n intentRouting: true\n commandRateLimitPolicy: hold\n commandRateLimitAiMaxPerWindow: 5\n commandRateLimitWindowHours: 24\n", { status: 200 }); } if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); // No repo permission at all -- the ONLY route to authorization is the confirmed_miner role on their own PR. diff --git a/test/unit/queue-lifecycle-guards.test.ts b/test/unit/queue-lifecycle-guards.test.ts index f89c1ea6cd..983228c9ab 100644 --- a/test/unit/queue-lifecycle-guards.test.ts +++ b/test/unit/queue-lifecycle-guards.test.ts @@ -1928,11 +1928,12 @@ describe("review-evasion protection (#review-evasion-protection)", () => { }, repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], }); - // reviewEvasionProtection/reviewEvasionComment are manifest-only now (Batch B, loopover#6443); pull any - // test override out of `overrides` before it reaches the DB write below so a per-test - // `{ reviewEvasionProtection: "off" }`/`{ reviewEvasionComment: false }` still takes effect via the - // manifest overlay instead of being silently outranked by the hardcoded default. - const { reviewEvasionProtection, reviewEvasionComment, ...dbOverrides } = overrides; + // reviewEvasionProtection/reviewEvasionComment (Batch B, loopover#6443) and autoCloseExemptLogins + // (loopover#6445) are manifest-only now; pull any test override out of `overrides` before it reaches + // the DB write below so a per-test `{ reviewEvasionProtection: "off" }`/`{ reviewEvasionComment: false }`/ + // `{ autoCloseExemptLogins: [...] }` still takes effect via the manifest overlay instead of being + // silently outranked by the hardcoded default. + const { reviewEvasionProtection, reviewEvasionComment, autoCloseExemptLogins, ...dbOverrides } = overrides; await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto" }, @@ -1946,6 +1947,7 @@ describe("review-evasion protection (#review-evasion-protection)", () => { checkRunMode: "off", reviewEvasionProtection: (reviewEvasionProtection as "off" | "close" | undefined) ?? "close", ...(reviewEvasionComment !== undefined ? { reviewEvasionComment: reviewEvasionComment as boolean } : {}), + ...(autoCloseExemptLogins !== undefined ? { autoCloseExemptLogins: autoCloseExemptLogins as string[] } : {}), }, }); } diff --git a/test/unit/repository-settings-project-milestone-match.test.ts b/test/unit/repository-settings-project-milestone-match.test.ts index 29e1261d14..d5947695b2 100644 --- a/test/unit/repository-settings-project-milestone-match.test.ts +++ b/test/unit/repository-settings-project-milestone-match.test.ts @@ -1,76 +1,67 @@ import { describe, expect, it } from "vitest"; import { getRepositorySettings, upsertRepositorySettings } from "../../src/db/repositories"; +import { resolveRepositorySettings } from "../../src/settings/repository-settings"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; import { createTestEnv } from "../helpers/d1"; -// #3183: autoProjectMilestoneMatch is the tri-state config for auto-project/milestone matching, mirroring the -// reviewCheckMode template (#2852). "off" is the conservative, opt-in default; "suggest" and "auto" currently -// behave identically (post a comment) since real milestone attachment isn't wired until #3185. -describe("repository_settings: autoProjectMilestoneMatch default + round-trip (#3183)", () => { +// #3183/loopover#6445: autoProjectMilestoneMatch is the tri-state config for auto-project/milestone matching, +// mirroring the reviewCheckMode template (#2852). "off" is the conservative, opt-in default; "suggest" and +// "auto" currently behave identically (post a comment) since real milestone attachment isn't wired until +// #3185. Moved off the DB entirely (config-as-code migration, loopover#6445) -- config-as-code only via +// .loopover.yml's settings: block now. +describe("repository_settings: autoProjectMilestoneMatch config-as-code (#3183)", () => { it("getRepositorySettings returns off for a repo with no DB row at all (conservative, opt-in default)", async () => { const env = createTestEnv(); const settings = await getRepositorySettings(env, "acme/brand-new-repo"); expect(settings.autoProjectMilestoneMatch).toBe("off"); }); - it("upsertRepositorySettings persists off when the caller omits the field entirely", async () => { + it("getRepositorySettings ignores a caller-supplied override on upsert -- always off (no DB column to persist it in)", async () => { const env = createTestEnv(); - await upsertRepositorySettings(env, { repoFullName: "acme/omits-field" }); - const settings = await getRepositorySettings(env, "acme/omits-field"); + await upsertRepositorySettings(env, { repoFullName: "acme/round-trip", autoProjectMilestoneMatch: "suggest" }); + const settings = await getRepositorySettings(env, "acme/round-trip"); expect(settings.autoProjectMilestoneMatch).toBe("off"); }); - it("an explicit suggest/auto opt-in round-trips through a re-upsert that carries it forward explicitly", async () => { + it("resolveRepositorySettings honors an explicit suggest/auto opt-in from .loopover.yml's settings: block", async () => { const env = createTestEnv(); - await upsertRepositorySettings(env, { repoFullName: "acme/round-trip", autoProjectMilestoneMatch: "suggest" }); - const settings = await getRepositorySettings(env, "acme/round-trip"); + await upsertRepositorySettings(env, { repoFullName: "acme/round-trip" }); + await upsertRepoFocusManifest(env, "acme/round-trip", { settings: { autoProjectMilestoneMatch: "suggest" } }); + const settings = await resolveRepositorySettings(env, "acme/round-trip"); expect(settings.autoProjectMilestoneMatch).toBe("suggest"); - // A true read-modify-write caller (the route-handler pattern: spread current settings, then override) must - // carry the persisted value forward explicitly -- upsertRepositorySettings never merges against the DB row. - await upsertRepositorySettings(env, { ...settings, repoFullName: "acme/round-trip" }); - const after = await getRepositorySettings(env, "acme/round-trip"); - expect(after.autoProjectMilestoneMatch).toBe("suggest"); }); - it("auto round-trips distinctly from suggest", async () => { + it("auto resolves distinctly from suggest via the manifest overlay", async () => { const env = createTestEnv(); - await upsertRepositorySettings(env, { repoFullName: "acme/auto-mode", autoProjectMilestoneMatch: "auto" }); - const settings = await getRepositorySettings(env, "acme/auto-mode"); + await upsertRepositorySettings(env, { repoFullName: "acme/auto-mode" }); + await upsertRepoFocusManifest(env, "acme/auto-mode", { settings: { autoProjectMilestoneMatch: "auto" } }); + const settings = await resolveRepositorySettings(env, "acme/auto-mode"); expect(settings.autoProjectMilestoneMatch).toBe("auto"); }); - - it("an invalid persisted DB value fails closed to off on read", async () => { - const env = createTestEnv(); - await upsertRepositorySettings(env, { repoFullName: "acme/malformed" }); - await env.DB.prepare("UPDATE repository_settings SET project_milestone_match_mode = ? WHERE repo_full_name = ?").bind("sometimes", "acme/malformed").run(); - const settings = await getRepositorySettings(env, "acme/malformed"); - expect(settings.autoProjectMilestoneMatch).toBe("off"); - }); }); -// #3186: autoProjectMilestoneMatchBackend selects which tracker the match/attach logic queries -- "github" -// (Milestones + Projects v2, the conservative default) or "linear" (an opted-in per-repo API key). -describe("repository_settings: autoProjectMilestoneMatchBackend default + round-trip (#3186)", () => { +// #3186/loopover#6445: autoProjectMilestoneMatchBackend selects which tracker the match/attach logic queries -- +// "github" (Milestones + Projects v2, the conservative default) or "linear" (an opted-in per-repo API key). +// Moved off the DB entirely (config-as-code migration, loopover#6445). +describe("repository_settings: autoProjectMilestoneMatchBackend config-as-code (#3186)", () => { it("getRepositorySettings returns github for a repo with no DB row at all", async () => { const env = createTestEnv(); const settings = await getRepositorySettings(env, "acme/brand-new-repo"); expect(settings.autoProjectMilestoneMatchBackend).toBe("github"); }); - it("an explicit linear opt-in round-trips through a re-upsert that carries it forward explicitly", async () => { + it("getRepositorySettings ignores a caller-supplied override on upsert -- always github (no DB column to persist it in)", async () => { const env = createTestEnv(); await upsertRepositorySettings(env, { repoFullName: "acme/linear-backend", autoProjectMilestoneMatchBackend: "linear" }); const settings = await getRepositorySettings(env, "acme/linear-backend"); - expect(settings.autoProjectMilestoneMatchBackend).toBe("linear"); - await upsertRepositorySettings(env, { ...settings, repoFullName: "acme/linear-backend" }); - const after = await getRepositorySettings(env, "acme/linear-backend"); - expect(after.autoProjectMilestoneMatchBackend).toBe("linear"); + expect(settings.autoProjectMilestoneMatchBackend).toBe("github"); }); - it("an invalid persisted DB value fails closed to github on read", async () => { + it("resolveRepositorySettings honors an explicit linear opt-in from .loopover.yml's settings: block", async () => { const env = createTestEnv(); - await upsertRepositorySettings(env, { repoFullName: "acme/malformed-backend" }); - await env.DB.prepare("UPDATE repository_settings SET auto_project_milestone_match_backend = ? WHERE repo_full_name = ?").bind("jira", "acme/malformed-backend").run(); - const settings = await getRepositorySettings(env, "acme/malformed-backend"); - expect(settings.autoProjectMilestoneMatchBackend).toBe("github"); + await upsertRepositorySettings(env, { repoFullName: "acme/linear-backend" }); + await upsertRepoFocusManifest(env, "acme/linear-backend", { settings: { autoProjectMilestoneMatchBackend: "linear" } }); + const settings = await resolveRepositorySettings(env, "acme/linear-backend"); + expect(settings.autoProjectMilestoneMatchBackend).toBe("linear"); }); });