diff --git a/src/queue/processors.ts b/src/queue/processors.ts index c45a1a4cb0..92f468e6cc 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -387,9 +387,10 @@ import { releaseTransientLockIfOwner, type TransientLockClaim, } from "./transient-locks"; -// #4013 step 1: temporary re-export shim so test/unit/queue.test.ts's existing +// #4013 step 1: temporary re-export shim so test/unit/queue.test.ts and its size-split siblings +// (queue-2/3/4/5.test.ts, queue-lifecycle-guards.test.ts)'s existing // `import { claimPrActuationLock, releasePrActuationLock } from "../../src/queue/processors"` keeps working -// unchanged -- those tests are deeply interspersed with unrelated ones in that file, not in a cleanly +// unchanged -- those tests are deeply interspersed with unrelated ones in that file family, not in a cleanly // extractable describe block, so relocating them is deliberately deferred rather than forced into this PR. export { claimPrActuationLock, releasePrActuationLock } from "./transient-locks"; import { isVisualPath } from "../review/visual/paths"; @@ -14418,8 +14419,8 @@ async function closeReviewEvasionSelfCloseIfActive( // rethrow) IS reachable and IS exercised by the existing re-close-failure tests -- only the else branch // (this `if`'s implicit non-Error path) and the fallback statement below are ignored. Concretely: a 500 // from GitHub on the re-close PATCH makes closePullRequest reject with a RequestError, which is exactly - // what test/unit/queue.test.ts's "REGRESSION (gate-flagged): a retry after the re-close failure - // converges -- the PR ends up closed, and the strike is recorded exactly once" drives through this `if`. + // what test/unit/queue-lifecycle-guards.test.ts's "REGRESSION (gate-flagged): a retry after the re-close + // failure converges -- the PR ends up closed, and the strike is recorded exactly once" drives through this `if`. /* v8 ignore else */ if (closeError instanceof Error) throw closeError; /* v8 ignore next -- unreachable, see above. */ diff --git a/test/unit/backfill-2.test.ts b/test/unit/backfill-2.test.ts new file mode 100644 index 0000000000..c37d6f6979 --- /dev/null +++ b/test/unit/backfill-2.test.ts @@ -0,0 +1,2565 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + getInstallationHealth, + listCheckSummaries, + listContributorRepoStats, + listIssues, + listLatestRepoGithubTotalsSnapshots, + listPullRequestFiles, + listPullRequestReviews, + listPullRequests, + listPullRequestDetailSyncStates, + listRecentMergedPullRequests, + upsertRecentMergedPullRequest, + listLatestGitHubRateLimitObservations, + listRepoLabels, + listRepoSyncSegments, + listRepoSyncStates, + persistRepoGithubTotalsSnapshot, + recordGitHubRateLimitObservation, + upsertInstallation, + upsertInstallationHealth, + upsertRepoSyncSegment, + upsertRepoSyncState, + getPullRequest, + upsertPullRequestFile, + upsertPullRequestFromGitHub, + upsertIssueFromGitHub, + upsertRepoLabel, + upsertRepositoryFromGitHub, + upsertRepositorySettings, +} from "../../src/db/repositories"; +import { + backfillOpenPullRequestDetails, + backfillRegisteredRepositories, + backfillRepositorySegment, + buildInstallationRepairDiagnostics, + enqueueRepositoryOpenDataBackfill, + enrichInstallationHealth, + fetchAndStorePullRequestFilesForReview, + fetchLinkedIssueFacts, + fetchLiveBaseBranchAdvancedAt, + fetchLiveCiAggregate, + fetchLiveReviewThreadBlockers, + fetchNamedCheckRunConclusion, + fetchRequiredStatusContexts, + isOwnReviewThreadAuthor, + isRateLimitedGitHubFailure, + mergeRequiredCiContexts, + reconcileOpenPullRequests, + refreshContributorActivity, + refreshInstallationHealth, + refreshPullRequestDetails, +} from "../../src/github/backfill"; +import { + clearGitHubResponseCacheForTest, + githubRateLimitAdmissionKeyForInstallation, + githubRateLimitAdmissionKeyForPublicToken, + setGitHubResponseCache, + type CachedGitHubResponse, +} from "../../src/github/client"; +import { GITTENSORY_CONTEXT_CHECK_NAME, GITTENSORY_GATE_CHECK_NAME, GITTENSORY_LEGACY_GATE_CHECK_NAME } from "../../src/review/check-names"; +import { normalizeRegistryPayload } from "../../src/registry/normalize"; +import { persistRegistrySnapshot } from "../../src/registry/sync"; +import { renderMetrics, resetMetrics } from "../../src/selfhost/metrics"; +import { createTestEnv } from "../helpers/d1"; + +// #4682 incident (2026-07-10): the stored-body cap used to be 4000 chars -- well under what a compliant +// screenshot-evidence table (or any sufficiently detailed PR/issue) actually needs -- and every body-content +// check (screenshotTableGate's matrix parser included) reads the STORED copy, not a live GitHub fetch, so a +// silently truncated body produced a false "missing evidence" close for a PR that had genuinely complete +// evidence. The cap now matches GitHub's own issue/PR body limit (65536) so it can only ever bind on content +// GitHub itself was never going to accept. + +async function seedRegisteredRepo(env: Env) { + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { + "JSONbored/gittensory": { + emission_share: 0.01, + issue_discovery_share: 0, + trusted_label_pipeline: true, + label_multipliers: { bug: 1.1, refactor: 0.5 }, + }, + }, + { kind: "raw-github", url: "https://example.test/master_repositories.json" }, + "2026-05-23T00:00:00.000Z", + ), + ); +} + +async function generatePrivateKeyPem(): Promise { + const key = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); + const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; +} + +async function persistTotalsSnapshot( + env: Env, + overrides: { + fetchedAt?: string; + sourceKind?: "github" | "installation"; + openIssuesTotal?: number; + openPullRequestsTotal?: number; + mergedPullRequestsTotal?: number; + closedUnmergedPullRequestsTotal?: number; + labelsTotal?: number; + } = {}, +) { + await persistRepoGithubTotalsSnapshot(env, { + id: crypto.randomUUID(), + repoFullName: "JSONbored/gittensory", + openIssuesTotal: overrides.openIssuesTotal ?? 0, + openPullRequestsTotal: overrides.openPullRequestsTotal ?? 0, + mergedPullRequestsTotal: overrides.mergedPullRequestsTotal ?? 0, + closedUnmergedPullRequestsTotal: overrides.closedUnmergedPullRequestsTotal ?? 0, + labelsTotal: overrides.labelsTotal ?? 0, + sourceKind: overrides.sourceKind ?? "github", + fetchedAt: overrides.fetchedAt ?? "2026-05-25T00:00:00.000Z", + payload: {}, + }); +} + +function githubTotalsResponse(counts: { openIssues: number; openPullRequests: number; mergedPullRequests: number; closedPullRequests: number; labels: number }) { + return Response.json({ + data: { + rateLimit: { remaining: 4999, resetAt: "2026-05-25T01:00:00.000Z" }, + repository: { + issues: { totalCount: counts.openIssues }, + openPullRequests: { totalCount: counts.openPullRequests }, + mergedPullRequests: { totalCount: counts.mergedPullRequests }, + closedPullRequests: { totalCount: counts.closedPullRequests }, + labels: { totalCount: counts.labels }, + }, + }, + }); +} + +describe("GitHub backfill", () => { + afterEach(() => { + vi.useRealTimers(); + clearGitHubResponseCacheForTest(); + vi.unstubAllGlobals(); + }); + + describe("fetchAndStorePullRequestFilesForReview", () => { + it("fetches the PR's files from GitHub, persists them, and returns the records", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/pulls/42/files")) { + return Response.json([ + { filename: "src/foo.ts", status: "modified", additions: 9, deletions: 2, changes: 11, patch: "@@ -1 +1 @@\n-old\n+new" }, + { filename: "README.md", status: "added", additions: 1, deletions: 0, changes: 1 }, + ]); + } + return new Response("not found", { status: 404 }); + }); + + const records = await fetchAndStorePullRequestFilesForReview(env, "JSONbored/gittensory", 42, "public-token"); + expect(records.map((r) => r.path)).toEqual(["src/foo.ts", "README.md"]); + expect(records[0]).toMatchObject({ path: "src/foo.ts", additions: 9, deletions: 2, status: "modified" }); + // Persisted: a subsequent stored read returns them (so the rest of the review run reuses them). + const stored = await listPullRequestFiles(env, "JSONbored/gittensory", 42); + expect(stored.map((r) => r.path).sort()).toEqual(["README.md", "src/foo.ts"]); + }); + + it("returns [] (and persists nothing) when GitHub returns no files — never throws", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => Response.json([])); + const records = await fetchAndStorePullRequestFilesForReview(env, "JSONbored/gittensory", 7, "public-token"); + expect(records).toEqual([]); + expect(await listPullRequestFiles(env, "JSONbored/gittensory", 7)).toEqual([]); + }); + + it("is fail-safe: a failed REST+GraphQL fetch returns [] rather than throwing", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => new Response("boom", { status: 500 })); + await expect(fetchAndStorePullRequestFilesForReview(env, "JSONbored/gittensory", 99, "public-token")).resolves.toEqual([]); + }); + }); + + describe("fetchLiveCiAggregate", () => { + it("reports unverified without fetching when the head SHA is missing", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const fetchSpy = vi.fn(); + vi.stubGlobal("fetch", fetchSpy); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", null, "public-token", null); + + expect(aggregate).toEqual({ ciState: "unverified", hasPending: false, hasVisiblePending: false, hasMissingRequiredContext: false, failingDetails: [], nonRequiredFailingDetails: [], ciCompletenessWarning: null }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("fails completed non-required red checks while still reporting optional pending visibility", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "trusted-required-ci", status: "completed", conclusion: "success" }, + { name: "attacker/non-required-check", status: "completed", conclusion: "failure", output: { title: "Injected failure" } }, + { name: "attacker/non-required-pending-check", status: "queued", conclusion: null }, + ], + }); + } + if (url.includes("/status?")) { + return Response.json({ + statuses: [ + { context: "trusted-required-ci", state: "success" }, + { context: "attacker/non-required-status", state: "failure", description: "Injected failure" }, + { context: "attacker/non-required-pending", state: "pending", description: "Never settles" }, + ], + }); + } + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["trusted-required-ci"])); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.hasPending).toBe(true); + expect(aggregate.hasVisiblePending).toBe(false); + expect(aggregate.failingDetails.map((detail) => detail.name).sort()).toEqual(["attacker/non-required-check", "attacker/non-required-status"]); + expect(aggregate.nonRequiredFailingDetails).toEqual([]); + }); + + it("treats a visible required classic status that is still pending as pending CI", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "lint", status: "completed", conclusion: "success" }, + ], + }); + } + if (url.includes("/status?")) { + return Response.json({ + statuses: [ + { context: "codecov/patch", state: "pending", description: "Waiting for report" }, + { context: "lint", state: "success" }, + ], + }); + } + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate( + env, + "JSONbored/gittensory", + "abc123", + "public-token", + new Set(["codecov/patch", "lint"]), + ); + + expect(aggregate.ciState).toBe("pending"); + expect(aggregate.hasPending).toBe(true); + expect(aggregate.hasVisiblePending).toBe(true); + expect(aggregate.failingDetails).toEqual([]); + }); + + it("a third-party app's COMPLETED action_required check-run fails closed as a manual-hold verdict", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "coverage", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "Contributor trust", status: "completed", conclusion: "action_required", app: { slug: "superagent-security" } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/awesome-claude", "sha4728", "public-token", new Set(["coverage", "Contributor trust"])); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.hasPending).toBe(false); + expect(aggregate.hasVisiblePending).toBe(false); + expect(aggregate.hasMissingRequiredContext).toBe(false); + expect(aggregate.failingDetails).toEqual([{ name: "Contributor trust" }]); + }); + + it("a third-party app's COMPLETED action_required check-run that IS a required context carries its summary/detailsUrl into failingDetails", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "coverage", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { + name: "Contributor trust", + status: "completed", + conclusion: "action_required", + app: { slug: "superagent-security" }, + output: { title: "Manual review needed" }, + details_url: "https://superagent.example/checks/contributor-trust", + }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/awesome-claude", "sha4729", "public-token", new Set(["coverage", "Contributor trust"])); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([ + { name: "Contributor trust", summary: "Manual review needed", detailsUrl: "https://superagent.example/checks/contributor-trust" }, + ]); + }); + + it("a third-party app's COMPLETED action_required check-run that is NOT a required context is held as a non-blocking advisory, never auto-closing the PR (#4414-regression)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "Superagent Security Scan", status: "completed", conclusion: "success", app: { slug: "superagent-security" } }, + { + name: "Contributor trust", + status: "completed", + conclusion: "action_required", + app: { slug: "superagent-security" }, + output: { title: "Manual review needed" }, + details_url: "https://superagent.example/checks/contributor-trust", + }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + // Matches real branch protection: only "validate" + "Superagent Security Scan" are required contexts -- + // "Contributor trust" is a SEPARATE, never-required check-run posted by the same app. + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "sha9001", "public-token", new Set(["validate", "Superagent Security Scan"])); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.hasPending).toBe(false); + expect(aggregate.failingDetails).toEqual([]); + expect(aggregate.nonRequiredFailingDetails).toEqual([ + { name: "Contributor trust", summary: "Manual review needed", detailsUrl: "https://superagent.example/checks/contributor-trust" }, + ]); + }); + + it("REGRESSION (#4812): a third-party action_required check-run on a repo with NO branch-protection required contexts configured at all is still non-blocking, not folded into failingDetails by the 'assume required when unknown' fallback", async () => { + // Reproduces PR #4812 (JSONbored/metagraphed) exactly: the repo's real branch protection returns + // required_status_checks.contexts: [] (confirmed via the live GitHub API) -- fetchRequiredStatusContexts + // maps that to an EMPTY Set, not null, so enforceRequiredOnly is false. Before this fix, isRequired()'s + // "!enforceRequiredOnly || ..." made every name "required" in that mode, silently reopening #4414 for + // any repo that simply never configured GitHub-native required status checks -- Contributor trust + // (Superagent's advisory, never-should-block signal) got folded into failingDetails and auto-closed a + // real contributor's PR with every actual CI check (tests, coverage, ui) green. + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "ui", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { + name: "Contributor trust", + status: "completed", + conclusion: "action_required", + app: { slug: "superagent-security" }, + output: { title: "Contributor flagged for review" }, + }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [{ context: "codecov/patch", state: "success" }] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "sha4812", "public-token", new Set()); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.failingDetails).toEqual([]); + expect(aggregate.nonRequiredFailingDetails).toEqual([{ name: "Contributor trust", summary: "Contributor flagged for review" }]); + }); + + it("REGRESSION (#4812): the same holds when required-status-context fetch outright failed (null), not just when it confirmed an empty list", async () => { + // A distinct origin from the empty-Set case above (a 403/fetch error rather than a confirmed-empty + // response), but must resolve the same way: no POSITIVE confirmation that Contributor trust is required + // means it stays advisory, never a close reason. + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "Contributor trust", status: "completed", conclusion: "action_required", app: { slug: "superagent-security" } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "sha4812b", "public-token", null); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.failingDetails).toEqual([]); + expect(aggregate.nonRequiredFailingDetails).toEqual([{ name: "Contributor trust" }]); + }); + + it("a non-required third-party action_required check-run with no output/details_url still lands in nonRequiredFailingDetails, bare (name-only)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "Contributor trust", status: "completed", conclusion: "action_required", app: { slug: "superagent-security" } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "sha9002", "public-token", new Set(["validate"])); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.failingDetails).toEqual([]); + expect(aggregate.nonRequiredFailingDetails).toEqual([{ name: "Contributor trust" }]); + }); + + it("a github-actions workflow awaiting 'Approve and run' (action_required) is still treated as pending, not settled (#fork-action-required)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [{ name: "build", status: "completed", conclusion: "action_required", app: { slug: "github-actions" } }], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "forksha", "public-token", new Set(["build"])); + + expect(aggregate.ciState).toBe("pending"); + expect(aggregate.hasPending).toBe(true); + expect(aggregate.hasVisiblePending).toBe(true); + expect(aggregate.failingDetails).toEqual([]); + }); + + it("an app-less check-run reporting action_required is conservatively treated as pending (unconfirmed app, not settled)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ check_runs: [{ name: "legacy-status-check", status: "completed", conclusion: "action_required" }] }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["legacy-status-check"])); + + expect(aggregate.hasPending).toBe(true); + expect(aggregate.hasVisiblePending).toBe(true); + }); + + it("a third-party app's action_required check-run that hasn't completed yet is still pending (not yet a settled verdict)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [{ name: "Contributor trust", status: "in_progress", conclusion: "action_required", app: { slug: "superagent-security" } }], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/awesome-claude", "sha", "public-token", new Set(["Contributor trust"])); + + expect(aggregate.hasPending).toBe(true); + expect(aggregate.hasVisiblePending).toBe(true); + }); + + it("keeps an observed failure failed while still reporting pending CI separately", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "test", status: "completed", conclusion: "failure", output: { title: "Test failed" } }, + { name: "coverage", status: "in_progress", conclusion: null }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.hasPending).toBe(true); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "test" })]); + }); + + it("falls back to gating all contexts when required contexts are unavailable", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); + if (url.includes("/status?")) return Response.json({ statuses: [{ context: "unknown-required-status", state: "failure", description: "Could be required" }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "unknown-required-status" })]); + expect(aggregate.nonRequiredFailingDetails).toEqual([]); + }); + + it("ignores ALL of the bot's OWN checks (Gate + Context) so it never self-deadlocks (#gate-self-deadlock)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "test", status: "completed", conclusion: "success" }, + // BOTH bot-posted checks, still in_progress (posted but not yet concluded). Counting EITHER would + // defer the very review that concludes it — the self-deadlock that froze green-CI PRs as "CI pending". + { name: "Gittensory Orb Review Agent", status: "in_progress", conclusion: null, app: { slug: "gittensory" } }, + { name: "Gittensory Gate", status: "in_progress", conclusion: null, app: { slug: "gittensory" } }, + { name: "Gittensory Context", status: "in_progress", conclusion: null, app: { slug: "gittensory" } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + // Both bot checks are excluded from the CI wait even if listed among the required contexts. + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "headsha", "public-token", new Set(["test", "Gittensory Orb Review Agent", "Gittensory Gate", "Gittensory Context"])); + + expect(aggregate.ciState).toBe("passed"); // would be "pending" if either in_progress bot check were counted + expect(aggregate.failingDetails).toEqual([]); + }); + + it("does not ignore same-named Gate check-runs from a different GitHub App", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "Gittensory Orb Review Agent", status: "completed", conclusion: "failure", output: { title: "External gate failed" }, app: { slug: "external-ci" } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test", "Gittensory Orb Review Agent"])); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "Gittensory Orb Review Agent", summary: "External gate failed" })]); + }); + + it("does not ignore classic statuses named like the Gate", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [{ context: "Gittensory Orb Review Agent", state: "failure", description: "External status failed" }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "Gittensory Orb Review Agent", summary: "External status failed" })]); + }); + + it("treats a required context that never ran (absent from results) as pending, not passed", async () => { + // Bypass: requiredContexts = {"validate"}, but CI only returns non-required checks (e.g. CodeQL). The + // "validate" job never triggered (fork workflow skipped, matrix split, etc.). Without the absent-check + // guard, total > 0 (CodeQL passed) → ciState = "passed" even though the required check never ran. + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + // Only non-required checks ran — "validate" is absent. + { name: "CodeQL", status: "completed", conclusion: "success", app: { slug: "github-advanced-security" } }, + { name: "Superagent Security Scan", status: "completed", conclusion: "success", app: { slug: "superagent" } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["validate"])); + + expect(aggregate.ciState).toBe("pending"); // required "validate" never ran — must not be "passed" + expect(aggregate.failingDetails).toEqual([]); + }); + + it("keeps bot-owned required contexts as seen (not absent) even though they are excluded from gate logic", async () => { + // The existing deadlock-avoidance test: bot-owned required contexts (Gate, Context) in in_progress are + // skipped from gate logic, but seenContextNames must still mark them to avoid the absent-check guard + // treating them as missing and re-introducing a false anyPending. + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "Gittensory Orb Review Agent", status: "in_progress", conclusion: null, app: { slug: "gittensory" } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "sha", "tok", new Set(["validate", "Gittensory Orb Review Agent"])); + + // "Gittensory Orb Review Agent" is a bot check: present in results (so not absent), excluded from gate logic → passed + expect(aggregate.ciState).toBe("passed"); + }); + + it("fold-all: a failed check-runs fetch with an otherwise-green status reads PENDING, not passed (fail-closed)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + // Transient check-runs fetch failure → githubJsonWithHeaders throws → caught → check set unread. + if (url.includes("/check-runs?")) return new Response("upstream error", { status: 500 }); + if (url.includes("/status?")) return Response.json({ statuses: [{ context: "ci/green", state: "success" }] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + // Without the fail-closed degrade this would be "passed" (one green status, no failing) — the seam. + expect(aggregate.ciState).toBe("pending"); + expect(aggregate.failingDetails).toEqual([]); + }); + + it("fold-all: a failed status fetch with an otherwise-green check-run reads PENDING, not passed (fail-closed)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success" }] }); + if (url.includes("/status?")) return new Response("upstream error", { status: 500 }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + expect(aggregate.ciState).toBe("pending"); + }); + + it("fold-all: a GitHub-Actions workflow AWAITING APPROVAL (suite not completed) reads PENDING, not passed (#ci-foldall-checksuites / #1799)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + // A fork PR awaiting CI approval: the required workflow never ran → no check-RUNS for it; only the + // always-on third-party checks posted (both pass) — the false-green seam. + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "Contributor trust", status: "completed", conclusion: "success", app: { slug: "superagent" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + // …but the check-SUITES show the GitHub-Actions workflow as `requested` (queued, awaiting approval). + if (url.includes("/check-suites?")) + return Response.json({ + check_suites: [ + { status: "requested", app: { slug: "github-actions" } }, + { status: "completed", app: { slug: "superagent" } }, + ], + }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "forksha", "public-token", null); + // Without this hardening the always-on passes alone read "passed" → a false-green approve. Now: pending → held. + expect(aggregate.ciState).toBe("pending"); + }); + + it("fold-all: all GitHub-Actions suites COMPLETED → still passed (no false-pending)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + expect(aggregate.ciState).toBe("passed"); + }); + + it("fold-all: waits for the required validate aggregate after its prerequisites settle", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) + return Response.json({ + check_runs: [ + { name: "CI / changes", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "CI / validate-code", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "CI / security", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + ], + }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + + expect(aggregate.ciState).toBe("pending"); + expect(aggregate.hasPending).toBe(true); + expect(aggregate.hasVisiblePending).toBe(false); + expect(aggregate.failingDetails).toEqual([]); + }); + + it("fold-all: passes once the validate aggregate check exists", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) + return Response.json({ + check_runs: [ + { name: "changes", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "validate-code", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "security", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + ], + }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.hasPending).toBe(false); + expect(aggregate.hasVisiblePending).toBe(false); + }); + + it("fold-all: an UNREADABLE check-suites read with NO first-party check-run reads PENDING, not passed (#review-audit / #1799)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + // Fork PR awaiting approval: only an always-on third-party status; NO first-party GitHub-Actions check-run. + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "license/cla", status: "completed", conclusion: "success", app: { slug: "cla-bot" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [{ context: "license/cla", state: "success" }] }); + if (url.includes("/check-suites?")) return new Response("forbidden", { status: 403 }); // same missing admin:read that forced fold-all + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "forksha", "public-token", null); + // The suites backstop is unreadable AND no first-party run was seen → cannot confirm CI ran → fail closed. + expect(aggregate.ciState).toBe("pending"); + }); + + it("fold-all: an UNREADABLE check-suites read still reads PASSED when a first-party check-run was seen (no over-pending)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + // A real (non-fork) PR: the GitHub-Actions workflow ran and passed (a first-party check-run is present). + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return new Response("forbidden", { status: 403 }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + expect(aggregate.ciState).toBe("passed"); // a first-party run was observed and passed; do not over-pend + }); + + it("surfaces a completeness warning when CI resolves to passed with no branch-protection required contexts, without changing ciState (#2137)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + // Workflow A ("test") ran and passed; workflow B (e.g. a path-filtered e2e-tests job) never triggered at + // all — no check-run, no check-suite entry, indistinguishable from a workflow that doesn't exist. + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + // Disposition is UNCHANGED (interim mitigation, not the full fix): a self-hosted repo with no + // expected-checks config would otherwise get stuck "pending" forever on a workflow that can structurally + // never complete. The gap is surfaced as an informational warning instead. + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.ciCompletenessWarning).toMatch(/branch-protection required checks/i); + }); + + it("does NOT surface a completeness warning when branch-protection required contexts ARE configured", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test"])); + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.ciCompletenessWarning).toBeNull(); + }); + + it("does NOT surface a completeness warning when ciState is anything other than passed", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "failure", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.ciCompletenessWarning).toBeNull(); + }); + + it("fold-all: a non-completed THIRD-PARTY suite is ignored (only first-party GitHub-Actions suites gate)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + // A third-party app's suite is perpetually "queued" — must NOT pend the gate (only github-actions counts). + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }, { status: "queued", app: { slug: "some-other-app" } }] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + expect(aggregate.ciState).toBe("passed"); + }); + + it("ENFORCE-required mode waits when the GitHub Actions suite is still materializing downstream jobs", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + let suitesFetched = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-suites?")) { + suitesFetched = true; + return Response.json({ check_suites: [{ status: "in_progress", app: { slug: "github-actions" } }] }); + } + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success" }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test"])); + expect(aggregate.ciState).toBe("pending"); + expect(aggregate.hasPending).toBe(true); + expect(suitesFetched).toBe(true); + }); + + it("ENFORCE-required mode treats suite-only optional pending as stale-cap eligible, not required-visible", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "in_progress", app: { slug: "github-actions" } }] }); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success" }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test"])); + + expect(aggregate.ciState).toBe("pending"); + expect(aggregate.hasPending).toBe(true); + expect(aggregate.hasVisiblePending).toBe(false); + }); + + it("ENFORCE-required mode does not over-pend when check-suites are unreadable after required checks passed", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return new Response("forbidden", { status: 403 }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test"])); + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.hasPending).toBe(false); + }); + + it("fold-all: tolerates malformed check-suites (missing app / missing status) without throwing", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "ci", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) + return Response.json({ + check_suites: [ + { status: "completed" }, // no app → app?.slug ?? "" = "" → not github-actions → ignored + { app: { slug: "github-actions" } }, // no status → status ?? "" = "" → not "completed" → pending + ], + }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + // The status-less github-actions suite is treated as not-completed (safe direction) → pending. + expect(aggregate.ciState).toBe("pending"); + }); + + it("an observed required failure stays FAILED even when a later check-runs page fetch fails", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?") && url.includes("&page=1")) { + return Response.json( + { check_runs: [{ name: "build", status: "completed", conclusion: "failure", output: { title: "boom" } }] }, + { headers: { link: '; rel="next"' } }, + ); + } + if (url.includes("/check-runs?")) return new Response("upstream error", { status: 500 }); // page 2 fails → incomplete + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + // Incomplete visibility does NOT override an authoritative observed failure. + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "build" })]); + }); + + it("reports unverified when both CI sources succeed but return no checks at all", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + expect(aggregate.ciState).toBe("unverified"); + }); + + it("treats a status response with no statuses field as empty (nullish-coalesce branch)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({}); // no `statuses` key → exercises `?? []` + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + expect(aggregate.ciState).toBe("passed"); + }); + + it("paginates commit-statuses so a failing status beyond page 1 is not silently dropped", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); + if (url.includes("/status?") && url.includes("&page=1")) { + return Response.json( + { statuses: [{ context: "ci/green", state: "success" }] }, + { headers: { link: '; rel="next"' } }, + ); + } + if (url.includes("/status?")) return Response.json({ statuses: [{ context: "ci/overflow", state: "failure", description: "page-2 failure" }] }); + return new Response("not found", { status: 404 }); + }); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "ci/overflow" })]); + }); + + describe("expectedCiContexts fallback (#selfhost-ci-verification)", () => { + it("passes with no completeness warning when branch protection is unreadable but an expected context settles clean", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success" }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const requiredContexts = mergeRequiredCiContexts(null, ["build"]); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); + + // The key regression: an expectedCiContexts fallback (used when branch protection can't be read) + // resolves to enforce-required mode, so a clean settle is "passed" with NO completeness warning — + // unlike the fold-all path, which would warn (#2137). + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.ciCompletenessWarning).toBeNull(); + }); + + it("stays pending when branch protection is unreadable and the expected context never appears on the commit", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const requiredContexts = mergeRequiredCiContexts(null, ["build"]); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); + + expect(aggregate.ciState).toBe("pending"); + // #selfhost-ci-deferral-staleness: a required context that never appeared is an INFERRED absence, not + // observed activity — distinct from hasVisiblePending, which stays false here (nothing is actively + // queued/in_progress; the context simply never posted at all). + expect(aggregate.hasMissingRequiredContext).toBe(true); + expect(aggregate.hasVisiblePending).toBe(false); + }); + + it("does not wait for absent bot-owned required contexts before the app can publish them", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success" }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const requiredContexts = mergeRequiredCiContexts(null, [ + "build", + GITTENSORY_GATE_CHECK_NAME, + GITTENSORY_LEGACY_GATE_CHECK_NAME, + GITTENSORY_CONTEXT_CHECK_NAME, + ]); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.hasPending).toBe(false); + expect(aggregate.hasMissingRequiredContext).toBe(false); + expect(aggregate.hasVisiblePending).toBe(false); + }); + + it("does NOT flag a missing required context as confidently absent when the check-runs page read was incomplete", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?") && url.includes("&page=1")) { + return Response.json( + { check_runs: [] }, + { headers: { link: '; rel="next"' } }, + ); + } + if (url.includes("/check-runs?")) return new Response("upstream error", { status: 500 }); // page 2 fails → incomplete + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const requiredContexts = mergeRequiredCiContexts(null, ["build"]); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); + + // "build" never appeared on the pages read, but the read did not COMPLETE — a partial page can't tell + // "never appears" from "appears on a page we didn't fetch", so this must NOT be a confident absence. + expect(aggregate.ciState).toBe("pending"); + expect(aggregate.hasMissingRequiredContext).toBe(false); + }); + + it("does not flag a missing NON-required context in fold-all mode (no branch protection, no expectedCiContexts)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + // No requiredContexts configured at all → fold-all mode (enforceRequiredOnly false); the + // missing-required-context signal only ever applies under enforceRequiredOnly. + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); + + expect(aggregate.hasMissingRequiredContext).toBe(false); + }); + + it("keeps hasVisiblePending authoritative when one required context is missing and another is actively queued", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "in_progress", conclusion: null }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + // "build" is actively queued (Class A); "deploy" is required but never appears (Class B) — both true at once. + const requiredContexts = mergeRequiredCiContexts(null, ["build", "deploy"]); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); + + expect(aggregate.hasVisiblePending).toBe(true); + expect(aggregate.hasMissingRequiredContext).toBe(true); + }); + + it("fails when branch protection is unreadable and the expected context completes red", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "failure" }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const requiredContexts = mergeRequiredCiContexts(null, ["build"]); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "build" })]); + }); + + it("does not regress the no-config case: no branch protection and no expected contexts still fold-all warns on pass", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const requiredContexts = mergeRequiredCiContexts(null, undefined); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.ciCompletenessWarning).toMatch(/branch-protection required checks/i); + }); + }); + + describe("duplicate-named check-runs from a re-run (dedupeLatestCheckRunsByName)", () => { + // Reproduces a real commit's shape: GitHub's /check-runs endpoint returned "Deploy UI preview version" TWICE + // after a "Re-run failed jobs" — id 85478132562 (conclusion: failure, started_at 2026-07-06T20:56:33Z, the + // STALE original run) and id 85485221438 (conclusion: skipped, started_at 2026-07-06T21:34:29Z, the CURRENT + // re-run). Without dedup, the stale failure alone flipped ciState to "failed" even though the check now + // passes — which fed a TERMINAL close signal into planAgentMaintenanceActions for a contributor PR whose CI + // had legitimately gone green on re-run. + it("keeps the NEWER (passing) conclusion when a re-run leaves a stale failing duplicate by name", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { id: 85478132562, name: "Deploy UI preview version", status: "completed", conclusion: "failure", started_at: "2026-07-06T20:56:33Z", check_suite: { id: 4401 } }, + { id: 85485221438, name: "Deploy UI preview version", status: "completed", conclusion: "skipped", started_at: "2026-07-06T21:34:29Z", check_suite: { id: 4401 } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "7d145f032eb3b03b5ac5868aa3cecf3e002bb6e2", "public-token", new Set(["Deploy UI preview version"])); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.failingDetails).toEqual([]); + }); + + it("still fails when the NEWER duplicate-named check-run is the one that failed (recency-aware, not duplicate-blind)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { id: 1, name: "Deploy UI preview version", status: "completed", conclusion: "success", started_at: "2026-07-06T20:56:33Z", check_suite: { id: 4401 } }, + { id: 2, name: "Deploy UI preview version", status: "completed", conclusion: "failure", started_at: "2026-07-06T21:34:29Z", check_suite: { id: 4401 } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["Deploy UI preview version"])); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "Deploy UI preview version" })]); + }); + + it("keeps the already-latest entry when a stale duplicate is listed OUT OF ORDER (appears second but started EARLIER)", async () => { + // GitHub does not document a stable ordering contract for /check-runs, so the comparison must genuinely + // compare timestamps rather than assume "later in the array is newer" — this fixture puts the STALE + // (older, failing) run SECOND to prove the earlier-started duplicate does not override the real latest. + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { id: 2, name: "Deploy UI preview version", status: "completed", conclusion: "skipped", started_at: "2026-07-06T21:34:29Z", check_suite: { id: 4401 } }, + { id: 1, name: "Deploy UI preview version", status: "completed", conclusion: "failure", started_at: "2026-07-06T20:56:33Z", check_suite: { id: 4401 } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["Deploy UI preview version"])); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.failingDetails).toEqual([]); + }); + + it("does not discard failing same-name check-runs from a different suite", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { id: 1, name: "security", status: "completed", conclusion: "failure", started_at: "2026-07-06T20:56:33Z", app: { slug: "required-security-ci" }, check_suite: { id: 9001 } }, + { id: 2, name: "security", status: "completed", conclusion: "success", started_at: "2026-07-06T21:34:29Z", app: { slug: "colliding-helper-ci" }, check_suite: { id: 9002 } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["security"])); + + expect(aggregate.ciState).toBe("failed"); + expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "security" })]); + }); + + it("falls back to array order when neither duplicate has a started_at (queued runs have none)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) { + return Response.json({ + check_runs: [ + { id: 1, name: "flaky", status: "completed", conclusion: "failure", started_at: null, check_suite: { id: 4401 } }, + { id: 2, name: "flaky", status: "completed", conclusion: "success", started_at: null, check_suite: { id: 4401 } }, + ], + }); + } + if (url.includes("/status?")) return Response.json({ statuses: [] }); + if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); + return new Response("not found", { status: 404 }); + }); + + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["flaky"])); + + // No timestamp to compare on either side → the later array entry wins (the documented tiebreak fallback). + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.failingDetails).toEqual([]); + }); + }); + }); + + describe("fetchLiveReviewThreadBlockers", () => { + it("returns unresolved non-outdated scanner review threads as blockers", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() === "https://api.github.com/graphql") { + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/signals/redaction.ts", + line: 30, + comments: { + nodes: [ + { + body: "\n**P1:** PUBLIC_LOCAL_PATH_INLINE regex fails to match Windows backslash paths", + url: "https://github.example/thread", + author: { login: "superagent-security[bot]" }, + }, + ], + }, + }, + ], + }, + }, + }, + }, + }); + } + return new Response("not found", { status: 404 }); + }); + + const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1748, "public-token"); + + expect(blockers).toEqual([ + expect.objectContaining({ + title: "PUBLIC_LOCAL_PATH_INLINE regex fails to match Windows backslash paths", + priority: "P1", + path: "src/signals/redaction.ts", + line: 30, + authorLogin: "superagent-security[bot]", + url: "https://github.example/thread", + scannerFinding: true, + }), + ]); + }); + + it("only trusts exact scanner bot logins for scanner-authored review thread blockers", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/superagent.ts", + line: 10, + comments: { nodes: [{ body: "**P1:** Canonical Superagent blocker", author: { login: "superagent[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/superagent-security.ts", + line: 20, + comments: { nodes: [{ body: "**P1:** Canonical Superagent Security blocker", author: { login: "superagent-security[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/superagent-security-dev.ts", + line: 30, + comments: { nodes: [{ body: "**P1:** Canonical Superagent Security Dev blocker", author: { login: "SUPERAGENT-SECURITY-DEV[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/brin.ts", + line: 40, + comments: { nodes: [{ body: "\n**P1:** Canonical Brin blocker", author: { login: "brin[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/superagentsecurity.ts", + line: 50, + comments: { nodes: [{ body: "**P1:** Typosquat without separator", author: { login: "superagentsecurity[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/superagent-evil.ts", + line: 60, + comments: { nodes: [{ body: "**P1:** Typosquat suffix", author: { login: "superagent-evil[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/brin-security.ts", + line: 70, + comments: { nodes: [{ body: "\n**P1:** Brin suffix typosquat", author: { login: "brin-security[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/missing-author.ts", + line: 80, + comments: { nodes: [{ body: "**P1:** Missing author cannot authorize", author: null }] }, + }, + ], + }, + }, + }, + }, + }); + }); + + const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); + + expect(blockers.map((blocker) => blocker.title)).toEqual([ + "Canonical Superagent blocker", + "Canonical Superagent Security blocker", + "Canonical Superagent Security Dev blocker", + "Canonical Brin blocker", + ]); + expect(blockers.map((blocker) => blocker.authorLogin)).toEqual(["superagent[bot]", "superagent-security[bot]", "SUPERAGENT-SECURITY-DEV[bot]", "brin[bot]"]); + expect(blockers.map((blocker) => blocker.path)).toEqual(["src/superagent.ts", "src/superagent-security.ts", "src/superagent-security-dev.ts", "src/brin.ts"]); + }); + + it("trusts self-host-configured TRUSTED_SCANNER_BOT_LOGINS additively alongside the built-in defaults (#4614)", async () => { + // Whitespace + case variation + an empty entry between commas -- exercises the trim/lowercase/filter + // handling, not just a bare exact match. + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token", TRUSTED_SCANNER_BOT_LOGINS: " CodeQL[bot] ,,Snyk-Security[bot]" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/codeql-finding.ts", + line: 5, + comments: { nodes: [{ body: "**P1:** Configured CodeQL blocker", author: { login: "codeql[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/snyk-finding.ts", + line: 15, + comments: { nodes: [{ body: "**P1:** Configured Snyk blocker", author: { login: "snyk-security[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/superagent-still-trusted.ts", + line: 25, + comments: { nodes: [{ body: "**P1:** Built-in default still trusted", author: { login: "superagent-security[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/unconfigured-scanner.ts", + line: 35, + comments: { nodes: [{ body: "**P1:** Unconfigured scanner stays untrusted", author: { login: "semgrep[bot]" }, authorAssociation: "NONE" }] }, + }, + ], + }, + }, + }, + }, + }); + }); + + const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1900, "public-token"); + + expect(blockers.map((blocker) => blocker.title)).toEqual(["Configured CodeQL blocker", "Configured Snyk blocker", "Built-in default still trusted"]); + expect(blockers.map((blocker) => blocker.authorLogin)).toEqual(["codeql[bot]", "snyk-security[bot]", "superagent-security[bot]"]); + }); + + it("ignores a whitespace-only TRUSTED_SCANNER_BOT_LOGINS override and keeps only the built-in defaults trusted", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token", TRUSTED_SCANNER_BOT_LOGINS: " " }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/codeql-finding.ts", + line: 5, + comments: { nodes: [{ body: "**P1:** Not configured, must not block", author: { login: "codeql[bot]" }, authorAssociation: "NONE" }] }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/superagent-still-trusted.ts", + line: 25, + comments: { nodes: [{ body: "**P1:** Built-in default still trusted", author: { login: "superagent-security[bot]" }, authorAssociation: "NONE" }] }, + }, + ], + }, + }, + }, + }, + }); + }); + + const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1901, "public-token"); + + expect(blockers.map((blocker) => blocker.authorLogin)).toEqual(["superagent-security[bot]"]); + }); + + it("paginates review threads so blockers beyond the first page cannot hide", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const queries: string[] = []; + const fetchSpy = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + const query = JSON.parse(String(init?.body)).query as string; + queries.push(query); + if (!query.includes("after:")) { + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [{ isResolved: true, isOutdated: false, path: "resolved.ts", line: 1, comments: { nodes: [{ body: "already resolved", author: { login: "superagent-security[bot]" } }] } }], + pageInfo: { hasNextPage: true, endCursor: "cursor-1" }, + }, + }, + }, + }, + }); + } + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/hidden.ts", + line: 77, + comments: { + nodes: [ + { + body: "**P0:** Hidden second-page review thread must block", + url: "https://github.example/thread/second-page", + author: { login: "superagent-security[bot]" }, + }, + ], + }, + }, + ], + pageInfo: { hasNextPage: false, endCursor: "cursor-2" }, + }, + }, + }, + }, + }); + }); + vi.stubGlobal("fetch", fetchSpy); + + const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); + + expect(fetchSpy).toHaveBeenCalledTimes(2); + expect(queries[0]).toContain("reviewThreads(first: 50)"); + expect(queries[1]).toContain('reviewThreads(first: 50, after: "cursor-1")'); + expect(blockers).toEqual([ + expect.objectContaining({ + title: "Hidden second-page review thread must block", + priority: "P0", + path: "src/hidden.ts", + line: 77, + url: "https://github.example/thread/second-page", + }), + ]); + }); + + it("stops review-thread pagination on a repeated cursor without dropping fetched blockers", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + let calls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + calls += 1; + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: + calls === 1 + ? [] + : [ + { + isResolved: false, + isOutdated: false, + path: "src/repeated-cursor.ts", + line: 9, + comments: { nodes: [{ body: "**P1:** Repeated cursor blocker", author: { login: "superagent-security[bot]" } }] }, + }, + ], + pageInfo: { hasNextPage: true, endCursor: "cursor-1" }, + }, + }, + }, + }, + }); + }); + + const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); + + expect(calls).toBe(2); + expect(blockers).toEqual([ + expect.objectContaining({ + title: "Repeated cursor blocker", + path: "src/repeated-cursor.ts", + line: 9, + }), + ]); + }); + + it("keeps fetched review-thread blockers when a later page is malformed", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + let calls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + calls += 1; + if (calls === 2) { + return Response.json({ data: { repository: { pullRequest: { reviewThreads: null } } } }); + } + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/fetched-before-malformed-page.ts", + line: 14, + comments: { nodes: [{ body: "**P1:** Fetched blocker before malformed page", author: { login: "superagent-security[bot]" } }] }, + }, + ], + pageInfo: { hasNextPage: true, endCursor: "cursor-1" }, + }, + }, + }, + }, + }); + }); + + const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); + + expect(calls).toBe(2); + expect(blockers).toEqual([ + expect.objectContaining({ + title: "Fetched blocker before malformed page", + path: "src/fetched-before-malformed-page.ts", + line: 14, + }), + ]); + }); + + it("stops review-thread pagination when GitHub omits the next cursor", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const fetchSpy = vi.fn(async (input: RequestInfo | URL) => { + if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/missing-cursor.ts", + line: 12, + comments: { nodes: [{ body: "**P2:** Missing cursor blocker", author: { login: "superagent-security[bot]" } }] }, + }, + ], + pageInfo: { hasNextPage: true, endCursor: null }, + }, + }, + }, + }, + }); + }); + vi.stubGlobal("fetch", fetchSpy); + + const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(blockers).toEqual([ + expect.objectContaining({ + title: "Missing cursor blocker", + path: "src/missing-cursor.ts", + line: 12, + }), + ]); + }); + + it("ignores unresolved review threads from untrusted public commenters", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/security.ts", + line: 42, + comments: { + nodes: [ + { + body: "\n**P0:** Forged public blocker", + url: "https://github.example/thread/untrusted", + author: { login: "random-outsider" }, + authorAssociation: "NONE", + }, + ], + }, + }, + ], + }, + }, + }, + }, + }); + }); + + await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token")).resolves.toEqual([]); + }); + + it("verifies member review thread authors against live repository permissions", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const permissionRequests: string[] = []; + const permissionUrl = (login: string) => `https://api.github.com/repos/JSONbored/gittensory/collaborators/${login}/permission`; + const permissionResponses = new Map Response>([ + [permissionUrl("repo-maintainer"), () => Response.json({ permission: "maintain" })], + [permissionUrl("repo-admin"), () => Response.json({ permission: "admin" })], + [permissionUrl("repo-writer"), () => Response.json({ permission: "write" })], + [permissionUrl("org-member"), () => Response.json({ permission: "read" })], + [permissionUrl("member-lookup-fails"), () => new Response("permission unavailable", { status: 403 })], + ]); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + const permissionResponse = permissionResponses.get(url); + if (permissionResponse) { + permissionRequests.push(url); + return permissionResponse(); + } + if (url !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { + isResolved: false, + isOutdated: false, + path: "src/maintainer-owner.ts", + line: 7, + comments: { + nodes: [ + { + body: "Owner requested change", + url: "https://github.example/thread/owner", + author: { login: "repo-owner" }, + authorAssociation: "OWNER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/maintainer-member.ts", + line: 8, + comments: { + nodes: [ + { + body: "Maintainer requested change", + url: "https://github.example/thread/maintainer", + author: { login: "repo-maintainer" }, + authorAssociation: "MEMBER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/maintainer-collaborator.ts", + line: 9, + comments: { + nodes: [ + { + body: "Collaborator requested change", + url: "https://github.example/thread/collaborator", + author: { login: "repo-collaborator" }, + authorAssociation: "COLLABORATOR", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/scanner.ts", + line: 10, + comments: { + nodes: [ + { + body: "Scanner requested change", + url: "https://github.example/thread/scanner", + author: { login: "superagent-security[bot]" }, + authorAssociation: "NONE", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/admin-member.ts", + line: 11, + comments: { + nodes: [ + { + body: "Admin requested change", + url: "https://github.example/thread/admin", + author: { login: "repo-admin" }, + authorAssociation: "MEMBER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/writer-member.ts", + line: 12, + comments: { + nodes: [ + { + body: "Writer requested change", + url: "https://github.example/thread/writer", + author: { login: "repo-writer" }, + authorAssociation: "MEMBER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/own-member.ts", + line: 13, + comments: { + nodes: [ + { + body: "Own bot requested change", + url: "https://github.example/thread/own-member", + author: { login: "gittensory-orb[bot]" }, + authorAssociation: "MEMBER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/maintainer-member-repeat.ts", + line: 14, + comments: { + nodes: [ + { + body: "Maintainer repeated change", + url: "https://github.example/thread/maintainer-repeat", + author: { login: "repo-maintainer" }, + authorAssociation: "MEMBER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/org-member.ts", + line: 15, + comments: { + nodes: [ + { + body: "Org member requested change", + url: "https://github.example/thread/org-member", + author: { login: "org-member" }, + authorAssociation: "MEMBER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/member-lookup-fails.ts", + line: 16, + comments: { + nodes: [ + { + body: "Unverified member requested change", + url: "https://github.example/thread/member-lookup-fails", + author: { login: "member-lookup-fails" }, + authorAssociation: "MEMBER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/member-missing-author.ts", + line: 17, + comments: { + nodes: [ + { + body: "Member association with missing author", + url: "https://github.example/thread/member-missing-author", + author: null, + authorAssociation: "MEMBER", + }, + ], + }, + }, + { + isResolved: false, + isOutdated: false, + path: "src/member-blank-author.ts", + line: 18, + comments: { + nodes: [ + { + body: "Member association with blank author", + url: "https://github.example/thread/member-blank-author", + author: { login: " " }, + authorAssociation: "MEMBER", + }, + ], + }, + }, + ], + }, + }, + }, + }, + }); + }); + + await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token")).resolves.toEqual([ + expect.objectContaining({ + title: "Owner requested change", + authorLogin: "repo-owner", + scannerFinding: false, + }), + expect.objectContaining({ + title: "Maintainer requested change", + authorLogin: "repo-maintainer", + scannerFinding: false, + }), + expect.objectContaining({ + title: "Collaborator requested change", + authorLogin: "repo-collaborator", + scannerFinding: false, + }), + expect.objectContaining({ + title: "Scanner requested change", + authorLogin: "superagent-security[bot]", + scannerFinding: false, + }), + expect.objectContaining({ + title: "Admin requested change", + authorLogin: "repo-admin", + scannerFinding: false, + }), + expect.objectContaining({ + title: "Writer requested change", + authorLogin: "repo-writer", + scannerFinding: false, + }), + expect.objectContaining({ + title: "Maintainer repeated change", + authorLogin: "repo-maintainer", + scannerFinding: false, + }), + ]); + expect(permissionRequests).toEqual([ + permissionUrl("repo-maintainer"), + permissionUrl("repo-admin"), + permissionUrl("repo-writer"), + permissionUrl("org-member"), + permissionUrl("member-lookup-fails"), + ]); + }); + + it("ignores resolved, outdated, own-bot, and empty review threads", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() === "https://api.github.com/graphql") { + return Response.json({ + data: { + repository: { + pullRequest: { + reviewThreads: { + nodes: [ + { isResolved: true, isOutdated: false, path: "a.ts", line: 1, comments: { nodes: [{ body: "resolved", author: { login: "superagent-security[bot]" } }] } }, + { isResolved: false, isOutdated: true, path: "b.ts", line: 2, comments: { nodes: [{ body: "outdated", author: { login: "superagent-security[bot]" } }] } }, + { isResolved: false, isOutdated: false, path: "c.ts", line: 3, comments: { nodes: [{ body: "own bot", author: { login: "gittensory-orb[bot]" }, authorAssociation: "OWNER" }] } }, + { isResolved: false, isOutdated: false, path: "own-collaborator.ts", line: 5, comments: { nodes: [{ body: "own bot with collaborator association", author: { login: "gittensory[bot]" }, authorAssociation: "COLLABORATOR" }] } }, + { isResolved: false, isOutdated: false, path: "no-comments.ts", line: 6, comments: null }, + { isResolved: false, isOutdated: false, path: "d.ts", line: 4, comments: { nodes: [{ body: " ", author: { login: "superagent-security[bot]" } }, null] } }, + null, + ], + }, + }, + }, + }, + }); + } + return new Response("not found", { status: 404 }); + }); + + await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1, "public-token")).resolves.toEqual([]); + }); + + it("fails open without a token, malformed repo name, or GraphQL response", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const fetchSpy = vi.fn(async () => new Response("boom", { status: 500 })); + vi.stubGlobal("fetch", fetchSpy); + + await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1, undefined)).resolves.toEqual([]); + expect(fetchSpy).not.toHaveBeenCalled(); + await expect(fetchLiveReviewThreadBlockers(env, "malformed", 1, "public-token")).resolves.toEqual([]); + expect(fetchSpy).not.toHaveBeenCalled(); + await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1, "public-token")).resolves.toEqual([]); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe("mergeRequiredCiContexts", () => { + it("unions branch-protection contexts with expectedCiContexts when both have entries", () => { + const merged = mergeRequiredCiContexts(new Set(["build"]), ["test", "lint"]); + expect([...(merged as Set)].sort()).toEqual(["build", "lint", "test"]); + }); + + it("returns branch-protection contexts unchanged when expectedCiContexts is undefined", () => { + const merged = mergeRequiredCiContexts(new Set(["build", "test"]), undefined); + expect(merged).toBeInstanceOf(Set); + expect([...(merged as Set)].sort()).toEqual(["build", "test"]); + }); + + it("returns branch-protection contexts unchanged when expectedCiContexts is an empty array", () => { + const merged = mergeRequiredCiContexts(new Set(["build", "test"]), []); + expect([...(merged as Set)].sort()).toEqual(["build", "test"]); + }); + + it("returns branch-protection contexts unchanged when expectedCiContexts is null", () => { + const merged = mergeRequiredCiContexts(new Set(["build", "test"]), null); + expect([...(merged as Set)].sort()).toEqual(["build", "test"]); + }); + + it("returns just the expected set when branch protection is null and expectedCiContexts has entries", () => { + const merged = mergeRequiredCiContexts(null, ["build"]); + expect([...(merged as Set)]).toEqual(["build"]); + }); + + it("returns null when branch protection is null and expectedCiContexts is undefined", () => { + expect(mergeRequiredCiContexts(null, undefined)).toBeNull(); + }); + + it("returns null when branch protection is null and expectedCiContexts is null", () => { + expect(mergeRequiredCiContexts(null, null)).toBeNull(); + }); + + it("returns null when branch protection is null and expectedCiContexts is an empty array", () => { + expect(mergeRequiredCiContexts(null, [])).toBeNull(); + }); + + it("returns just the expected set when branch protection is an empty (non-null) Set and expectedCiContexts has entries", () => { + const merged = mergeRequiredCiContexts(new Set(), ["build"]); + expect([...(merged as Set)]).toEqual(["build"]); + }); + + it("drops blank/whitespace-only expectedCiContexts entries while keeping real entries", () => { + const merged = mergeRequiredCiContexts(null, [" ", "", "build"]); + expect([...(merged as Set)]).toEqual(["build"]); + }); + + it("trims leading/trailing whitespace from expectedCiContexts entries in the result", () => { + const merged = mergeRequiredCiContexts(null, [" build "]); + expect([...(merged as Set)]).toEqual(["build"]); + }); + }); + + describe("fetchRequiredStatusContexts", () => { + it("returns null without fetching when baseRef is missing", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const fetchSpy = vi.fn(); + vi.stubGlobal("fetch", fetchSpy); + expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", null, "public-token")).toBeNull(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("returns the live required set when branch protection is readable (both contexts and checks shapes)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/protection/required_status_checks")) { + return Response.json({ contexts: ["validate", "", null], checks: [{ context: "Superagent Security Scan" }, { context: " " }] }); + } + return new Response("not found", { status: 404 }); + }); + const required = await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token"); + expect([...(required as Set)].sort()).toEqual(["Superagent Security Scan", "validate"]); + }); + + it("uses the shared GitHub GET cache for raw branch-protection reads without double-counting rate-limit observations", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const store = new Map(); + setGitHubResponseCache({ + get: async (key) => store.get(key) ?? null, + set: async (key, value) => void store.set(key, value), + }); + let fetches = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + fetches += 1; + expect(input.toString()).toContain("/branches/main/protection/required_status_checks"); + return Response.json( + { contexts: ["validate"], checks: [] }, + { headers: { "x-ratelimit-limit": "5000", "x-ratelimit-remaining": "4999", "x-ratelimit-reset": "1782802800" } }, + ); + }); + + // admissionKey mirrors the real caller (processors.ts), which always resolves + passes its own admission + // key -- omitting it here would exercise the (deliberately unpersisted) no-attribution path instead of + // this test's actual point: that a cache hit does not double-record telemetry for the SAME bucket. + const admissionKey = githubRateLimitAdmissionKeyForPublicToken(); + const first = await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token", admissionKey); + const second = await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token", admissionKey); + + expect([...(first as Set)]).toEqual(["validate"]); + expect([...(second as Set)]).toEqual(["validate"]); + expect(fetches).toBe(1); + expect([...store.keys()].some((key) => key.includes("/branches/main/protection/required_status_checks"))).toBe(true); + const observations = await listLatestGitHubRateLimitObservations(env); + expect(observations).toHaveLength(1); + expect(observations[0]).toMatchObject({ + repoFullName: "JSONbored/gittensory", + resource: "rest", + path: "/branches/main/protection/required_status_checks", + statusCode: 200, + remaining: 4999, + admissionKey, + }); + }); + + it("returns null when the live read fails, even if a stale global fallback is configured (conservative fold-all)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + (env as Env & { GITTENSORY_REQUIRED_CI_CONTEXTS?: string }).GITTENSORY_REQUIRED_CI_CONTEXTS = "stale-required-context"; + vi.stubGlobal("fetch", async () => new Response("forbidden", { status: 403 })); + expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); + }); + + it("classifies a bare 403 (no admin:read) as permission-denied, not a rate limit (#selfhost-runtime-pressure)", async () => { + resetMetrics(); + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => new Response("forbidden", { status: 403 })); + expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); + expect(await renderMetrics()).toContain("gittensory_github_branch_protection_permission_denied_total 1"); + }); + + it("does not count a 404 (no branch protection configured) as permission-denied", async () => { + resetMetrics(); + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); + expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); + expect(await renderMetrics()).not.toContain("gittensory_github_branch_protection_permission_denied_total"); + }); + + it("does not count a genuinely rate-limited 403 (x-ratelimit-remaining: 0) as permission-denied", async () => { + resetMetrics(); + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal( + "fetch", + async () => + new Response("secondary rate limit", { + status: 403, + headers: { "x-ratelimit-remaining": "0", "x-ratelimit-reset": "1780000000" }, + }), + ); + expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); + expect(await renderMetrics()).not.toContain("gittensory_github_branch_protection_permission_denied_total"); + }, 15_000); + }); + + describe("fetchNamedCheckRunConclusion (#2564)", () => { + it("returns undefined without fetching when headSha is missing", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const fetchSpy = vi.fn(); + vi.stubGlobal("fetch", fetchSpy); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", null, "CLA Assistant Lite", "cla-assistant", "public-token")).toBeUndefined(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("returns the lowercased conclusion for a matching check-run (case-insensitive name match)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + expect(input.toString()).toContain("/commits/sha1/check-runs"); + return Response.json({ total_count: 1, check_runs: [{ id: 1, name: "cla assistant lite", status: "completed", conclusion: "SUCCESS", app: { slug: "cla-assistant" } }] }); + }); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBe("success"); + }); + + it("REGRESSION (gate finding): returns null (deterministic missing), not undefined (transient), without fetching when no trusted app slug is configured — a check-run-only config with no slug must still BLOCK, not silently hold forever", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + const fetchSpy = vi.fn(); + vi.stubGlobal("fetch", fetchSpy); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", null, "public-token")).toBeNull(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("ignores a completed same-name check-run from an untrusted app slug", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => + Response.json({ + total_count: 1, + check_runs: [{ id: 1, name: "CLA Assistant Lite", status: "completed", conclusion: "success", app: { slug: "github-actions" } }], + }), + ); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeNull(); + }); + + it("uses the trusted producer when spoofed and trusted same-name runs both exist", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => + Response.json({ + total_count: 2, + check_runs: [ + { id: 1, name: "CLA Assistant Lite", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { id: 2, name: "CLA Assistant Lite", status: "completed", conclusion: "failure", app: { slug: "cla-assistant" } }, + ], + }), + ); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBe("failure"); + }); + + it("returns null (resolved: not found) when the head SHA has no check-run with that name", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => Response.json({ total_count: 1, check_runs: [{ id: 1, name: "Some Other Check", status: "completed", conclusion: "success", app: { slug: "cla-assistant" } }] })); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeNull(); + }); + + it("returns null (resolved: not found) when the response omits check_runs entirely (nullish fallback)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => Response.json({ total_count: 0 })); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeNull(); + }); + + // #2564 gate-review finding: a matching check-run that has NOT finished yet must resolve to undefined + // (unresolved), not "" — an in-progress run's conclusion:null means "not decided yet," not "resolved with + // an empty conclusion." Coercing it to "" made claMode: block hard-fail a PR before the named check had + // actually finished running. + it("returns undefined (unresolved) for a matching but still-in-progress check-run (status !== completed, conclusion: null)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => Response.json({ total_count: 1, check_runs: [{ id: 1, name: "CLA Assistant Lite", status: "in_progress", conclusion: null, app: { slug: "cla-assistant" } }] })); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeUndefined(); + }); + + it("returns an empty string for a matching, COMPLETED check-run with an unexpected empty conclusion (genuine edge case, not the in-progress case)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => Response.json({ total_count: 1, check_runs: [{ id: 1, name: "CLA Assistant Lite", status: "completed", conclusion: null, app: { slug: "cla-assistant" } }] })); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBe(""); + }); + + it("returns undefined (not evaluated) when the fetch fails, never a false 'missing'", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async () => new Response("forbidden", { status: 403 })); + expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeUndefined(); + }); + }); + + describe("fetchLinkedIssueFacts (#2136)", () => { + it("returns a found result with the extracted facts, falling back to the requested number and open state when the payload omits them", async () => { + const env = createTestEnv({}); + // Sparse payload: no `number`, no `state` — exercises the `data.number ?? issueNumber` and + // `data.state ?? "open"` defensive fallbacks. + vi.stubGlobal("fetch", async () => Response.json({ labels: [{ name: "bug" }, "manual-string-label"], assignees: [{ login: "maintainer" }], user: { login: "reporter" } })); + const result = await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 42, "tok"); + expect(result).toEqual({ + status: "found", + facts: { number: 42, labels: ["bug", "manual-string-label"], assignees: ["maintainer"], state: "open", authorLogin: "reporter", title: null, body: null, closedAt: null }, + }); + }); + + it("extracts title + body (#1961/#3906, linked-issue satisfaction assessment) from the same REST payload — no second fetch", async () => { + const env = createTestEnv({}); + vi.stubGlobal("fetch", async () => + Response.json({ + number: 1275, + state: "open", + labels: [], + assignees: [], + user: { login: "reporter" }, + title: "Enrich SN74 Gittensor — add SSE stream", + body: "We need a live SSE stream surface for SN74 Gittensor.", + }), + ); + const result = await fetchLinkedIssueFacts(env, "JSONbored/metagraphed", 1275, "tok"); + expect(result).toEqual({ + status: "found", + facts: { + number: 1275, + labels: [], + assignees: [], + state: "open", + authorLogin: "reporter", + title: "Enrich SN74 Gittensor — add SSE stream", + body: "We need a live SSE stream surface for SN74 Gittensor.", + closedAt: null, + }, + }); + }); + + it("extracts closedAt (#4528) from the same REST payload when the issue is closed", async () => { + const env = createTestEnv({}); + vi.stubGlobal("fetch", async () => + Response.json({ number: 4279, state: "closed", closed_at: "2026-07-09T22:15:14Z" }), + ); + const result = await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 4279, "tok"); + expect(result.status === "found" && result.facts.closedAt).toBe("2026-07-09T22:15:14Z"); + }); + + it("falls back to null for closedAt (#4528) when the payload omits it or it isn't a string", async () => { + const env = createTestEnv({}); + vi.stubGlobal("fetch", async () => Response.json({ number: 4279, state: "open", closed_at: null })); + const result = await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 4279, "tok"); + expect(result.status === "found" && result.facts.closedAt).toBeNull(); + }); + + it("falls back to null for title/body when the payload omits them or they are empty strings", async () => { + const env = createTestEnv({}); + vi.stubGlobal("fetch", async () => Response.json({ number: 7, state: "open", title: "", body: "" })); + const result = await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 7, "tok"); + expect(result.status).toBe("found"); + expect(result.status === "found" && result.facts.title).toBeNull(); + expect(result.status === "found" && result.facts.body).toBeNull(); + }); + + it("returns not_found on a confirmed 404, distinct from a transient fetch error", async () => { + const env = createTestEnv({}); + vi.stubGlobal("fetch", async () => new Response("missing", { status: 404 })); + expect(await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 999999, "tok")).toEqual({ status: "not_found" }); + }); + + it("REGRESSION: treats a 404 seen with the public/anonymous token as fetch_error, not not_found — GitHub also returns 404 for a real but inaccessible private issue", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-tok" }); + vi.stubGlobal("fetch", async () => new Response("missing", { status: 404 })); + // The public token proves nothing about repo access, so a 404 here could just as easily mean "this issue + // is real but private and this token can't see it" -- treating it as CONFIRMED absence risks closing a PR + // over a genuinely-linked issue. + expect(await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 42, env.GITHUB_PUBLIC_TOKEN)).toEqual({ status: "fetch_error" }); + }); + + it("REGRESSION: treats a 404 seen with no token at all as fetch_error, not not_found", async () => { + const env = createTestEnv({}); + vi.stubGlobal("fetch", async () => new Response("missing", { status: 404 })); + expect(await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 42, undefined)).toEqual({ status: "fetch_error" }); + }); + + it("returns fetch_error on a transient failure (5xx), never conflating it with not_found", async () => { + const env = createTestEnv({}); + vi.stubGlobal("fetch", async () => new Response("server error", { status: 500 })); + expect(await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 42, "tok")).toEqual({ status: "fetch_error" }); + }); + }); + + describe("isRateLimitedGitHubFailure", () => { + it("does not treat a bare permission 403 (remaining > 0, no Retry-After, no secondary body) as a rate limit", () => { + expect( + isRateLimitedGitHubFailure({ statusCode: 403, retryAfter: null, remaining: "4999", body: "Resource not accessible by integration" }), + ).toBe(false); + }); + + it("treats a 403 with an exhausted x-ratelimit-remaining as a rate limit", () => { + expect(isRateLimitedGitHubFailure({ statusCode: 403, retryAfter: null, remaining: "0", body: "" })).toBe(true); + }); + + it("treats a 403 or 429 carrying a Retry-After header as a rate limit", () => { + expect(isRateLimitedGitHubFailure({ statusCode: 403, retryAfter: "60", remaining: "100", body: "" })).toBe(true); + expect(isRateLimitedGitHubFailure({ statusCode: 429, retryAfter: "1", remaining: null, body: "" })).toBe(true); + }); + + it("treats a secondary-limit / abuse body as a rate limit", () => { + expect( + isRateLimitedGitHubFailure({ statusCode: 403, retryAfter: null, remaining: "100", body: "You have exceeded a secondary rate limit" }), + ).toBe(true); + }); + + it("does not treat a 429 without any rate-limit signal as a rate limit", () => { + expect(isRateLimitedGitHubFailure({ statusCode: 429, retryAfter: null, remaining: "100", body: "" })).toBe(false); + }); + + it("does not treat a non-403/429 failure as a rate limit even with a matching body", () => { + expect(isRateLimitedGitHubFailure({ statusCode: 500, retryAfter: null, remaining: null, body: "secondary rate limit" })).toBe(false); + }); + }); + + describe("reconcileOpenPullRequests (#audit-open-pr-reconciliation)", () => { + it("returns an all-zero result for a repo that does not exist", async () => { + const env = createTestEnv(); + expect(await reconcileOpenPullRequests(env, "owner/missing")).toEqual({ repoFullName: "owner/missing", remoteOpenCount: 0, localOpenCount: 0, missingNumbers: [] }); + }); + + it("reports no missing numbers when the local table already has every remote-open PR", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + await seedRegisteredRepo(env); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/pulls?")) return Response.json([{ number: 1 }]); + return Response.json([]); + }); + + const result = await reconcileOpenPullRequests(env, "JSONbored/gittensory"); + + expect(result).toEqual({ repoFullName: "JSONbored/gittensory", remoteOpenCount: 1, localOpenCount: 1, missingNumbers: [] }); + }); + + it("REGRESSION (#3782/#3793): reports a PR number GitHub has open that has no local row at all", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + await seedRegisteredRepo(env); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/pulls?")) return Response.json([{ number: 1 }, { number: 7 }]); // #7 opened but never made it into the local table + return Response.json([]); + }); + + const result = await reconcileOpenPullRequests(env, "JSONbored/gittensory"); + + expect(result).toEqual({ repoFullName: "JSONbored/gittensory", remoteOpenCount: 2, localOpenCount: 1, missingNumbers: [7] }); + }); + + it("paginates the open-PR list past the first 100 via the Link header", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + await seedRegisteredRepo(env); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/pulls?")) { + const page = Number(new URL(url).searchParams.get("page") ?? "1"); + if (page === 1) { + return Response.json( + Array.from({ length: 100 }, (_, i) => ({ number: i + 1 })), + { headers: { link: '; rel="next"' } }, + ); + } + return Response.json([{ number: 101 }]); + } + return Response.json([]); + }); + + const result = await reconcileOpenPullRequests(env, "JSONbored/gittensory"); + + expect(result.remoteOpenCount).toBe(101); + expect(result.missingNumbers).toEqual(expect.arrayContaining([1, 101])); + }); + + it("fails open (all-zero result) when the FIRST page fails, so a GitHub hiccup never falsely reports every local PR as missing", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + await seedRegisteredRepo(env); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); + vi.stubGlobal("fetch", async () => new Response("down", { status: 500 })); + + expect(await reconcileOpenPullRequests(env, "JSONbored/gittensory")).toEqual({ repoFullName: "JSONbored/gittensory", remoteOpenCount: 0, localOpenCount: 0, missingNumbers: [] }); + }); + + it("keeps the pages already fetched when a LATER page fails mid-crawl (a partial remote list can only under-report, never falsely flag a real local PR)", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + await seedRegisteredRepo(env); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/pulls?")) { + const page = Number(new URL(url).searchParams.get("page") ?? "1"); + if (page === 1) { + return Response.json( + Array.from({ length: 100 }, (_, i) => ({ number: i + 1 })), + { headers: { link: '; rel="next"' } }, + ); + } + return new Response("down", { status: 500 }); + } + return Response.json([]); + }); + + const result = await reconcileOpenPullRequests(env, "JSONbored/gittensory"); + + expect(result.remoteOpenCount).toBe(100); // page 1's 100 items are kept despite page 2 failing + }); + }); + +}); + +describe("isOwnReviewThreadAuthor", () => { + const env = createTestEnv(); // GITHUB_APP_SLUG defaults to "gittensory" (test/helpers/d1.ts) + + it("matches our own gittensory app bot logins by prefix", () => { + for (const login of ["gittensory[bot]", "gittensory-orb[bot]", "gittensory-review[bot]", "GITTENSORY[bot]", "gittensory", "gittensory-orb"]) { + expect(isOwnReviewThreadAuthor(env, login)).toBe(true); + } + }); + + it("does not match a third-party bot whose slug only ends in -gittensory[bot] (regression)", () => { + // A `\b` boundary also fires after a hyphen, so the unanchored regex misclassified these external bots as + // our own author and dropped their review-thread comments as self-authored non-blockers (fail-open). + for (const login of ["evil-gittensory[bot]", "x-gittensory[bot]", "not-gittensory", "gittensory-fork"]) { + expect(isOwnReviewThreadAuthor(env, login)).toBe(false); + } + }); + + it("treats an absent login as not our own author", () => { + expect(isOwnReviewThreadAuthor(env, null)).toBe(false); + expect(isOwnReviewThreadAuthor(env, undefined)).toBe(false); + expect(isOwnReviewThreadAuthor(env, "")).toBe(false); + }); + + it("derives the match from GITHUB_APP_SLUG (#4615), not a hardcoded literal", () => { + const renamed = createTestEnv({ GITHUB_APP_SLUG: "acme-review" }); + expect(isOwnReviewThreadAuthor(renamed, "acme-review[bot]")).toBe(true); + expect(isOwnReviewThreadAuthor(renamed, "acme-review-orb[bot]")).toBe(true); + expect(isOwnReviewThreadAuthor(renamed, "acme-review")).toBe(true); + // The OLD slug no longer matches once an operator renames their App -- proves the literal is gone. + expect(isOwnReviewThreadAuthor(renamed, "gittensory[bot]")).toBe(false); + }); + + it("a slug containing regex metacharacters is escaped, not interpreted (defensive)", () => { + const weird = createTestEnv({ GITHUB_APP_SLUG: "acme.bot" }); + expect(isOwnReviewThreadAuthor(weird, "acme.bot[bot]")).toBe(true); + expect(isOwnReviewThreadAuthor(weird, "acmexbot[bot]")).toBe(false); // "." must not act as a wildcard + }); + + it("fails closed when GITHUB_APP_SLUG is blank (misconfiguration)", () => { + const blank = createTestEnv({ GITHUB_APP_SLUG: "" }); + expect(isOwnReviewThreadAuthor(blank, "gittensory[bot]")).toBe(false); + expect(isOwnReviewThreadAuthor(blank, "")).toBe(false); + }); +}); + diff --git a/test/unit/backfill.test.ts b/test/unit/backfill.test.ts index 76c915f0de..d348398669 100644 --- a/test/unit/backfill.test.ts +++ b/test/unit/backfill.test.ts @@ -70,6 +70,7 @@ import { createTestEnv } from "../helpers/d1"; // silently truncated body produced a false "missing evidence" close for a PR that had genuinely complete // evidence. The cap now matches GitHub's own issue/PR body limit (65536) so it can only ever bind on content // GitHub itself was never going to accept. + describe("pull request / issue body storage cap (#4682 regression)", () => { it("stores a body well past the OLD 4000-char cap in full, unmangled", async () => { const env = createTestEnv(); @@ -183,6 +184,81 @@ describe("pull request / issue body storage cap (#4682 regression)", () => { }); }); +async function seedRegisteredRepo(env: Env) { + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { + "JSONbored/gittensory": { + emission_share: 0.01, + issue_discovery_share: 0, + trusted_label_pipeline: true, + label_multipliers: { bug: 1.1, refactor: 0.5 }, + }, + }, + { kind: "raw-github", url: "https://example.test/master_repositories.json" }, + "2026-05-23T00:00:00.000Z", + ), + ); +} + +async function generatePrivateKeyPem(): Promise { + const key = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); + const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; +} + +async function persistTotalsSnapshot( + env: Env, + overrides: { + fetchedAt?: string; + sourceKind?: "github" | "installation"; + openIssuesTotal?: number; + openPullRequestsTotal?: number; + mergedPullRequestsTotal?: number; + closedUnmergedPullRequestsTotal?: number; + labelsTotal?: number; + } = {}, +) { + await persistRepoGithubTotalsSnapshot(env, { + id: crypto.randomUUID(), + repoFullName: "JSONbored/gittensory", + openIssuesTotal: overrides.openIssuesTotal ?? 0, + openPullRequestsTotal: overrides.openPullRequestsTotal ?? 0, + mergedPullRequestsTotal: overrides.mergedPullRequestsTotal ?? 0, + closedUnmergedPullRequestsTotal: overrides.closedUnmergedPullRequestsTotal ?? 0, + labelsTotal: overrides.labelsTotal ?? 0, + sourceKind: overrides.sourceKind ?? "github", + fetchedAt: overrides.fetchedAt ?? "2026-05-25T00:00:00.000Z", + payload: {}, + }); +} + +function githubTotalsResponse(counts: { openIssues: number; openPullRequests: number; mergedPullRequests: number; closedPullRequests: number; labels: number }) { + return Response.json({ + data: { + rateLimit: { remaining: 4999, resetAt: "2026-05-25T01:00:00.000Z" }, + repository: { + issues: { totalCount: counts.openIssues }, + openPullRequests: { totalCount: counts.openPullRequests }, + mergedPullRequests: { totalCount: counts.mergedPullRequests }, + closedPullRequests: { totalCount: counts.closedPullRequests }, + labels: { totalCount: counts.labels }, + }, + }, + }); +} + describe("GitHub backfill", () => { afterEach(() => { vi.useRealTimers(); @@ -190,6 +266,7 @@ describe("GitHub backfill", () => { vi.unstubAllGlobals(); }); + it("fetches the fresh base branch tip timestamp without replaying the commit response cache", async () => { const env = createTestEnv(); const cacheGet = vi.fn(async () => ({ @@ -4388,2487 +4465,4 @@ describe("GitHub backfill", () => { // FIX B: the review path uses this to fetch + persist a PR's files inline when the stored rows are still // empty (the PR-opened webhook beat the async detail-sync), so the FIRST AI review/grounding/comment sees // the real diff instead of "0 files". - describe("fetchAndStorePullRequestFilesForReview", () => { - it("fetches the PR's files from GitHub, persists them, and returns the records", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/pulls/42/files")) { - return Response.json([ - { filename: "src/foo.ts", status: "modified", additions: 9, deletions: 2, changes: 11, patch: "@@ -1 +1 @@\n-old\n+new" }, - { filename: "README.md", status: "added", additions: 1, deletions: 0, changes: 1 }, - ]); - } - return new Response("not found", { status: 404 }); - }); - - const records = await fetchAndStorePullRequestFilesForReview(env, "JSONbored/gittensory", 42, "public-token"); - expect(records.map((r) => r.path)).toEqual(["src/foo.ts", "README.md"]); - expect(records[0]).toMatchObject({ path: "src/foo.ts", additions: 9, deletions: 2, status: "modified" }); - // Persisted: a subsequent stored read returns them (so the rest of the review run reuses them). - const stored = await listPullRequestFiles(env, "JSONbored/gittensory", 42); - expect(stored.map((r) => r.path).sort()).toEqual(["README.md", "src/foo.ts"]); - }); - - it("returns [] (and persists nothing) when GitHub returns no files — never throws", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => Response.json([])); - const records = await fetchAndStorePullRequestFilesForReview(env, "JSONbored/gittensory", 7, "public-token"); - expect(records).toEqual([]); - expect(await listPullRequestFiles(env, "JSONbored/gittensory", 7)).toEqual([]); - }); - - it("is fail-safe: a failed REST+GraphQL fetch returns [] rather than throwing", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => new Response("boom", { status: 500 })); - await expect(fetchAndStorePullRequestFilesForReview(env, "JSONbored/gittensory", 99, "public-token")).resolves.toEqual([]); - }); - }); - - describe("fetchLiveCiAggregate", () => { - it("reports unverified without fetching when the head SHA is missing", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const fetchSpy = vi.fn(); - vi.stubGlobal("fetch", fetchSpy); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", null, "public-token", null); - - expect(aggregate).toEqual({ ciState: "unverified", hasPending: false, hasVisiblePending: false, hasMissingRequiredContext: false, failingDetails: [], nonRequiredFailingDetails: [], ciCompletenessWarning: null }); - expect(fetchSpy).not.toHaveBeenCalled(); - }); - - it("fails completed non-required red checks while still reporting optional pending visibility", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "trusted-required-ci", status: "completed", conclusion: "success" }, - { name: "attacker/non-required-check", status: "completed", conclusion: "failure", output: { title: "Injected failure" } }, - { name: "attacker/non-required-pending-check", status: "queued", conclusion: null }, - ], - }); - } - if (url.includes("/status?")) { - return Response.json({ - statuses: [ - { context: "trusted-required-ci", state: "success" }, - { context: "attacker/non-required-status", state: "failure", description: "Injected failure" }, - { context: "attacker/non-required-pending", state: "pending", description: "Never settles" }, - ], - }); - } - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["trusted-required-ci"])); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.hasPending).toBe(true); - expect(aggregate.hasVisiblePending).toBe(false); - expect(aggregate.failingDetails.map((detail) => detail.name).sort()).toEqual(["attacker/non-required-check", "attacker/non-required-status"]); - expect(aggregate.nonRequiredFailingDetails).toEqual([]); - }); - - it("treats a visible required classic status that is still pending as pending CI", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "lint", status: "completed", conclusion: "success" }, - ], - }); - } - if (url.includes("/status?")) { - return Response.json({ - statuses: [ - { context: "codecov/patch", state: "pending", description: "Waiting for report" }, - { context: "lint", state: "success" }, - ], - }); - } - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate( - env, - "JSONbored/gittensory", - "abc123", - "public-token", - new Set(["codecov/patch", "lint"]), - ); - - expect(aggregate.ciState).toBe("pending"); - expect(aggregate.hasPending).toBe(true); - expect(aggregate.hasVisiblePending).toBe(true); - expect(aggregate.failingDetails).toEqual([]); - }); - - it("a third-party app's COMPLETED action_required check-run fails closed as a manual-hold verdict", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "coverage", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "Contributor trust", status: "completed", conclusion: "action_required", app: { slug: "superagent-security" } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/awesome-claude", "sha4728", "public-token", new Set(["coverage", "Contributor trust"])); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.hasPending).toBe(false); - expect(aggregate.hasVisiblePending).toBe(false); - expect(aggregate.hasMissingRequiredContext).toBe(false); - expect(aggregate.failingDetails).toEqual([{ name: "Contributor trust" }]); - }); - - it("a third-party app's COMPLETED action_required check-run that IS a required context carries its summary/detailsUrl into failingDetails", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "coverage", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { - name: "Contributor trust", - status: "completed", - conclusion: "action_required", - app: { slug: "superagent-security" }, - output: { title: "Manual review needed" }, - details_url: "https://superagent.example/checks/contributor-trust", - }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/awesome-claude", "sha4729", "public-token", new Set(["coverage", "Contributor trust"])); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([ - { name: "Contributor trust", summary: "Manual review needed", detailsUrl: "https://superagent.example/checks/contributor-trust" }, - ]); - }); - - it("a third-party app's COMPLETED action_required check-run that is NOT a required context is held as a non-blocking advisory, never auto-closing the PR (#4414-regression)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "Superagent Security Scan", status: "completed", conclusion: "success", app: { slug: "superagent-security" } }, - { - name: "Contributor trust", - status: "completed", - conclusion: "action_required", - app: { slug: "superagent-security" }, - output: { title: "Manual review needed" }, - details_url: "https://superagent.example/checks/contributor-trust", - }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - // Matches real branch protection: only "validate" + "Superagent Security Scan" are required contexts -- - // "Contributor trust" is a SEPARATE, never-required check-run posted by the same app. - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "sha9001", "public-token", new Set(["validate", "Superagent Security Scan"])); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.hasPending).toBe(false); - expect(aggregate.failingDetails).toEqual([]); - expect(aggregate.nonRequiredFailingDetails).toEqual([ - { name: "Contributor trust", summary: "Manual review needed", detailsUrl: "https://superagent.example/checks/contributor-trust" }, - ]); - }); - - it("REGRESSION (#4812): a third-party action_required check-run on a repo with NO branch-protection required contexts configured at all is still non-blocking, not folded into failingDetails by the 'assume required when unknown' fallback", async () => { - // Reproduces PR #4812 (JSONbored/metagraphed) exactly: the repo's real branch protection returns - // required_status_checks.contexts: [] (confirmed via the live GitHub API) -- fetchRequiredStatusContexts - // maps that to an EMPTY Set, not null, so enforceRequiredOnly is false. Before this fix, isRequired()'s - // "!enforceRequiredOnly || ..." made every name "required" in that mode, silently reopening #4414 for - // any repo that simply never configured GitHub-native required status checks -- Contributor trust - // (Superagent's advisory, never-should-block signal) got folded into failingDetails and auto-closed a - // real contributor's PR with every actual CI check (tests, coverage, ui) green. - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "ui", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { - name: "Contributor trust", - status: "completed", - conclusion: "action_required", - app: { slug: "superagent-security" }, - output: { title: "Contributor flagged for review" }, - }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [{ context: "codecov/patch", state: "success" }] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "sha4812", "public-token", new Set()); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.failingDetails).toEqual([]); - expect(aggregate.nonRequiredFailingDetails).toEqual([{ name: "Contributor trust", summary: "Contributor flagged for review" }]); - }); - - it("REGRESSION (#4812): the same holds when required-status-context fetch outright failed (null), not just when it confirmed an empty list", async () => { - // A distinct origin from the empty-Set case above (a 403/fetch error rather than a confirmed-empty - // response), but must resolve the same way: no POSITIVE confirmation that Contributor trust is required - // means it stays advisory, never a close reason. - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "Contributor trust", status: "completed", conclusion: "action_required", app: { slug: "superagent-security" } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "sha4812b", "public-token", null); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.failingDetails).toEqual([]); - expect(aggregate.nonRequiredFailingDetails).toEqual([{ name: "Contributor trust" }]); - }); - - it("a non-required third-party action_required check-run with no output/details_url still lands in nonRequiredFailingDetails, bare (name-only)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "Contributor trust", status: "completed", conclusion: "action_required", app: { slug: "superagent-security" } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "sha9002", "public-token", new Set(["validate"])); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.failingDetails).toEqual([]); - expect(aggregate.nonRequiredFailingDetails).toEqual([{ name: "Contributor trust" }]); - }); - - it("a github-actions workflow awaiting 'Approve and run' (action_required) is still treated as pending, not settled (#fork-action-required)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [{ name: "build", status: "completed", conclusion: "action_required", app: { slug: "github-actions" } }], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "forksha", "public-token", new Set(["build"])); - - expect(aggregate.ciState).toBe("pending"); - expect(aggregate.hasPending).toBe(true); - expect(aggregate.hasVisiblePending).toBe(true); - expect(aggregate.failingDetails).toEqual([]); - }); - - it("an app-less check-run reporting action_required is conservatively treated as pending (unconfirmed app, not settled)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ check_runs: [{ name: "legacy-status-check", status: "completed", conclusion: "action_required" }] }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["legacy-status-check"])); - - expect(aggregate.hasPending).toBe(true); - expect(aggregate.hasVisiblePending).toBe(true); - }); - - it("a third-party app's action_required check-run that hasn't completed yet is still pending (not yet a settled verdict)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [{ name: "Contributor trust", status: "in_progress", conclusion: "action_required", app: { slug: "superagent-security" } }], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/awesome-claude", "sha", "public-token", new Set(["Contributor trust"])); - - expect(aggregate.hasPending).toBe(true); - expect(aggregate.hasVisiblePending).toBe(true); - }); - - it("keeps an observed failure failed while still reporting pending CI separately", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "test", status: "completed", conclusion: "failure", output: { title: "Test failed" } }, - { name: "coverage", status: "in_progress", conclusion: null }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.hasPending).toBe(true); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "test" })]); - }); - - it("falls back to gating all contexts when required contexts are unavailable", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); - if (url.includes("/status?")) return Response.json({ statuses: [{ context: "unknown-required-status", state: "failure", description: "Could be required" }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "unknown-required-status" })]); - expect(aggregate.nonRequiredFailingDetails).toEqual([]); - }); - - it("ignores ALL of the bot's OWN checks (Gate + Context) so it never self-deadlocks (#gate-self-deadlock)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "test", status: "completed", conclusion: "success" }, - // BOTH bot-posted checks, still in_progress (posted but not yet concluded). Counting EITHER would - // defer the very review that concludes it — the self-deadlock that froze green-CI PRs as "CI pending". - { name: "Gittensory Orb Review Agent", status: "in_progress", conclusion: null, app: { slug: "gittensory" } }, - { name: "Gittensory Gate", status: "in_progress", conclusion: null, app: { slug: "gittensory" } }, - { name: "Gittensory Context", status: "in_progress", conclusion: null, app: { slug: "gittensory" } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - // Both bot checks are excluded from the CI wait even if listed among the required contexts. - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "headsha", "public-token", new Set(["test", "Gittensory Orb Review Agent", "Gittensory Gate", "Gittensory Context"])); - - expect(aggregate.ciState).toBe("passed"); // would be "pending" if either in_progress bot check were counted - expect(aggregate.failingDetails).toEqual([]); - }); - - it("does not ignore same-named Gate check-runs from a different GitHub App", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "Gittensory Orb Review Agent", status: "completed", conclusion: "failure", output: { title: "External gate failed" }, app: { slug: "external-ci" } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test", "Gittensory Orb Review Agent"])); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "Gittensory Orb Review Agent", summary: "External gate failed" })]); - }); - - it("does not ignore classic statuses named like the Gate", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [{ context: "Gittensory Orb Review Agent", state: "failure", description: "External status failed" }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "Gittensory Orb Review Agent", summary: "External status failed" })]); - }); - - it("treats a required context that never ran (absent from results) as pending, not passed", async () => { - // Bypass: requiredContexts = {"validate"}, but CI only returns non-required checks (e.g. CodeQL). The - // "validate" job never triggered (fork workflow skipped, matrix split, etc.). Without the absent-check - // guard, total > 0 (CodeQL passed) → ciState = "passed" even though the required check never ran. - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - // Only non-required checks ran — "validate" is absent. - { name: "CodeQL", status: "completed", conclusion: "success", app: { slug: "github-advanced-security" } }, - { name: "Superagent Security Scan", status: "completed", conclusion: "success", app: { slug: "superagent" } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["validate"])); - - expect(aggregate.ciState).toBe("pending"); // required "validate" never ran — must not be "passed" - expect(aggregate.failingDetails).toEqual([]); - }); - - it("keeps bot-owned required contexts as seen (not absent) even though they are excluded from gate logic", async () => { - // The existing deadlock-avoidance test: bot-owned required contexts (Gate, Context) in in_progress are - // skipped from gate logic, but seenContextNames must still mark them to avoid the absent-check guard - // treating them as missing and re-introducing a false anyPending. - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "Gittensory Orb Review Agent", status: "in_progress", conclusion: null, app: { slug: "gittensory" } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "sha", "tok", new Set(["validate", "Gittensory Orb Review Agent"])); - - // "Gittensory Orb Review Agent" is a bot check: present in results (so not absent), excluded from gate logic → passed - expect(aggregate.ciState).toBe("passed"); - }); - - it("fold-all: a failed check-runs fetch with an otherwise-green status reads PENDING, not passed (fail-closed)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - // Transient check-runs fetch failure → githubJsonWithHeaders throws → caught → check set unread. - if (url.includes("/check-runs?")) return new Response("upstream error", { status: 500 }); - if (url.includes("/status?")) return Response.json({ statuses: [{ context: "ci/green", state: "success" }] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - // Without the fail-closed degrade this would be "passed" (one green status, no failing) — the seam. - expect(aggregate.ciState).toBe("pending"); - expect(aggregate.failingDetails).toEqual([]); - }); - - it("fold-all: a failed status fetch with an otherwise-green check-run reads PENDING, not passed (fail-closed)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success" }] }); - if (url.includes("/status?")) return new Response("upstream error", { status: 500 }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - expect(aggregate.ciState).toBe("pending"); - }); - - it("fold-all: a GitHub-Actions workflow AWAITING APPROVAL (suite not completed) reads PENDING, not passed (#ci-foldall-checksuites / #1799)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - // A fork PR awaiting CI approval: the required workflow never ran → no check-RUNS for it; only the - // always-on third-party checks posted (both pass) — the false-green seam. - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "Contributor trust", status: "completed", conclusion: "success", app: { slug: "superagent" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - // …but the check-SUITES show the GitHub-Actions workflow as `requested` (queued, awaiting approval). - if (url.includes("/check-suites?")) - return Response.json({ - check_suites: [ - { status: "requested", app: { slug: "github-actions" } }, - { status: "completed", app: { slug: "superagent" } }, - ], - }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "forksha", "public-token", null); - // Without this hardening the always-on passes alone read "passed" → a false-green approve. Now: pending → held. - expect(aggregate.ciState).toBe("pending"); - }); - - it("fold-all: all GitHub-Actions suites COMPLETED → still passed (no false-pending)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - expect(aggregate.ciState).toBe("passed"); - }); - - it("fold-all: waits for the required validate aggregate after its prerequisites settle", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) - return Response.json({ - check_runs: [ - { name: "CI / changes", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "CI / validate-code", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "CI / security", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - ], - }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - - expect(aggregate.ciState).toBe("pending"); - expect(aggregate.hasPending).toBe(true); - expect(aggregate.hasVisiblePending).toBe(false); - expect(aggregate.failingDetails).toEqual([]); - }); - - it("fold-all: passes once the validate aggregate check exists", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) - return Response.json({ - check_runs: [ - { name: "changes", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "validate-code", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "security", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - ], - }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.hasPending).toBe(false); - expect(aggregate.hasVisiblePending).toBe(false); - }); - - it("fold-all: an UNREADABLE check-suites read with NO first-party check-run reads PENDING, not passed (#review-audit / #1799)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - // Fork PR awaiting approval: only an always-on third-party status; NO first-party GitHub-Actions check-run. - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "license/cla", status: "completed", conclusion: "success", app: { slug: "cla-bot" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [{ context: "license/cla", state: "success" }] }); - if (url.includes("/check-suites?")) return new Response("forbidden", { status: 403 }); // same missing admin:read that forced fold-all - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/metagraphed", "forksha", "public-token", null); - // The suites backstop is unreadable AND no first-party run was seen → cannot confirm CI ran → fail closed. - expect(aggregate.ciState).toBe("pending"); - }); - - it("fold-all: an UNREADABLE check-suites read still reads PASSED when a first-party check-run was seen (no over-pending)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - // A real (non-fork) PR: the GitHub-Actions workflow ran and passed (a first-party check-run is present). - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return new Response("forbidden", { status: 403 }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - expect(aggregate.ciState).toBe("passed"); // a first-party run was observed and passed; do not over-pend - }); - - it("surfaces a completeness warning when CI resolves to passed with no branch-protection required contexts, without changing ciState (#2137)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - // Workflow A ("test") ran and passed; workflow B (e.g. a path-filtered e2e-tests job) never triggered at - // all — no check-run, no check-suite entry, indistinguishable from a workflow that doesn't exist. - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - // Disposition is UNCHANGED (interim mitigation, not the full fix): a self-hosted repo with no - // expected-checks config would otherwise get stuck "pending" forever on a workflow that can structurally - // never complete. The gap is surfaced as an informational warning instead. - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.ciCompletenessWarning).toMatch(/branch-protection required checks/i); - }); - - it("does NOT surface a completeness warning when branch-protection required contexts ARE configured", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test"])); - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.ciCompletenessWarning).toBeNull(); - }); - - it("does NOT surface a completeness warning when ciState is anything other than passed", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "failure", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.ciCompletenessWarning).toBeNull(); - }); - - it("fold-all: a non-completed THIRD-PARTY suite is ignored (only first-party GitHub-Actions suites gate)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - // A third-party app's suite is perpetually "queued" — must NOT pend the gate (only github-actions counts). - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }, { status: "queued", app: { slug: "some-other-app" } }] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - expect(aggregate.ciState).toBe("passed"); - }); - - it("ENFORCE-required mode waits when the GitHub Actions suite is still materializing downstream jobs", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - let suitesFetched = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-suites?")) { - suitesFetched = true; - return Response.json({ check_suites: [{ status: "in_progress", app: { slug: "github-actions" } }] }); - } - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success" }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test"])); - expect(aggregate.ciState).toBe("pending"); - expect(aggregate.hasPending).toBe(true); - expect(suitesFetched).toBe(true); - }); - - it("ENFORCE-required mode treats suite-only optional pending as stale-cap eligible, not required-visible", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "in_progress", app: { slug: "github-actions" } }] }); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success" }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test"])); - - expect(aggregate.ciState).toBe("pending"); - expect(aggregate.hasPending).toBe(true); - expect(aggregate.hasVisiblePending).toBe(false); - }); - - it("ENFORCE-required mode does not over-pend when check-suites are unreadable after required checks passed", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return new Response("forbidden", { status: 403 }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["test"])); - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.hasPending).toBe(false); - }); - - it("fold-all: tolerates malformed check-suites (missing app / missing status) without throwing", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "ci", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) - return Response.json({ - check_suites: [ - { status: "completed" }, // no app → app?.slug ?? "" = "" → not github-actions → ignored - { app: { slug: "github-actions" } }, // no status → status ?? "" = "" → not "completed" → pending - ], - }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - // The status-less github-actions suite is treated as not-completed (safe direction) → pending. - expect(aggregate.ciState).toBe("pending"); - }); - - it("an observed required failure stays FAILED even when a later check-runs page fetch fails", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?") && url.includes("&page=1")) { - return Response.json( - { check_runs: [{ name: "build", status: "completed", conclusion: "failure", output: { title: "boom" } }] }, - { headers: { link: '; rel="next"' } }, - ); - } - if (url.includes("/check-runs?")) return new Response("upstream error", { status: 500 }); // page 2 fails → incomplete - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - // Incomplete visibility does NOT override an authoritative observed failure. - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "build" })]); - }); - - it("reports unverified when both CI sources succeed but return no checks at all", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - expect(aggregate.ciState).toBe("unverified"); - }); - - it("treats a status response with no statuses field as empty (nullish-coalesce branch)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({}); // no `statuses` key → exercises `?? []` - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - expect(aggregate.ciState).toBe("passed"); - }); - - it("paginates commit-statuses so a failing status beyond page 1 is not silently dropped", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); - if (url.includes("/status?") && url.includes("&page=1")) { - return Response.json( - { statuses: [{ context: "ci/green", state: "success" }] }, - { headers: { link: '; rel="next"' } }, - ); - } - if (url.includes("/status?")) return Response.json({ statuses: [{ context: "ci/overflow", state: "failure", description: "page-2 failure" }] }); - return new Response("not found", { status: 404 }); - }); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "ci/overflow" })]); - }); - - describe("expectedCiContexts fallback (#selfhost-ci-verification)", () => { - it("passes with no completeness warning when branch protection is unreadable but an expected context settles clean", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success" }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const requiredContexts = mergeRequiredCiContexts(null, ["build"]); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); - - // The key regression: an expectedCiContexts fallback (used when branch protection can't be read) - // resolves to enforce-required mode, so a clean settle is "passed" with NO completeness warning — - // unlike the fold-all path, which would warn (#2137). - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.ciCompletenessWarning).toBeNull(); - }); - - it("stays pending when branch protection is unreadable and the expected context never appears on the commit", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const requiredContexts = mergeRequiredCiContexts(null, ["build"]); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); - - expect(aggregate.ciState).toBe("pending"); - // #selfhost-ci-deferral-staleness: a required context that never appeared is an INFERRED absence, not - // observed activity — distinct from hasVisiblePending, which stays false here (nothing is actively - // queued/in_progress; the context simply never posted at all). - expect(aggregate.hasMissingRequiredContext).toBe(true); - expect(aggregate.hasVisiblePending).toBe(false); - }); - - it("does not wait for absent bot-owned required contexts before the app can publish them", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success" }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const requiredContexts = mergeRequiredCiContexts(null, [ - "build", - GITTENSORY_GATE_CHECK_NAME, - GITTENSORY_LEGACY_GATE_CHECK_NAME, - GITTENSORY_CONTEXT_CHECK_NAME, - ]); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.hasPending).toBe(false); - expect(aggregate.hasMissingRequiredContext).toBe(false); - expect(aggregate.hasVisiblePending).toBe(false); - }); - - it("does NOT flag a missing required context as confidently absent when the check-runs page read was incomplete", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?") && url.includes("&page=1")) { - return Response.json( - { check_runs: [] }, - { headers: { link: '; rel="next"' } }, - ); - } - if (url.includes("/check-runs?")) return new Response("upstream error", { status: 500 }); // page 2 fails → incomplete - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const requiredContexts = mergeRequiredCiContexts(null, ["build"]); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); - - // "build" never appeared on the pages read, but the read did not COMPLETE — a partial page can't tell - // "never appears" from "appears on a page we didn't fetch", so this must NOT be a confident absence. - expect(aggregate.ciState).toBe("pending"); - expect(aggregate.hasMissingRequiredContext).toBe(false); - }); - - it("does not flag a missing NON-required context in fold-all mode (no branch protection, no expectedCiContexts)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - // No requiredContexts configured at all → fold-all mode (enforceRequiredOnly false); the - // missing-required-context signal only ever applies under enforceRequiredOnly. - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", null); - - expect(aggregate.hasMissingRequiredContext).toBe(false); - }); - - it("keeps hasVisiblePending authoritative when one required context is missing and another is actively queued", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "in_progress", conclusion: null }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - // "build" is actively queued (Class A); "deploy" is required but never appears (Class B) — both true at once. - const requiredContexts = mergeRequiredCiContexts(null, ["build", "deploy"]); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); - - expect(aggregate.hasVisiblePending).toBe(true); - expect(aggregate.hasMissingRequiredContext).toBe(true); - }); - - it("fails when branch protection is unreadable and the expected context completes red", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "failure" }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const requiredContexts = mergeRequiredCiContexts(null, ["build"]); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "build" })]); - }); - - it("does not regress the no-config case: no branch protection and no expected contexts still fold-all warns on pass", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "test", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const requiredContexts = mergeRequiredCiContexts(null, undefined); - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.ciCompletenessWarning).toMatch(/branch-protection required checks/i); - }); - }); - - describe("duplicate-named check-runs from a re-run (dedupeLatestCheckRunsByName)", () => { - // Reproduces a real commit's shape: GitHub's /check-runs endpoint returned "Deploy UI preview version" TWICE - // after a "Re-run failed jobs" — id 85478132562 (conclusion: failure, started_at 2026-07-06T20:56:33Z, the - // STALE original run) and id 85485221438 (conclusion: skipped, started_at 2026-07-06T21:34:29Z, the CURRENT - // re-run). Without dedup, the stale failure alone flipped ciState to "failed" even though the check now - // passes — which fed a TERMINAL close signal into planAgentMaintenanceActions for a contributor PR whose CI - // had legitimately gone green on re-run. - it("keeps the NEWER (passing) conclusion when a re-run leaves a stale failing duplicate by name", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { id: 85478132562, name: "Deploy UI preview version", status: "completed", conclusion: "failure", started_at: "2026-07-06T20:56:33Z", check_suite: { id: 4401 } }, - { id: 85485221438, name: "Deploy UI preview version", status: "completed", conclusion: "skipped", started_at: "2026-07-06T21:34:29Z", check_suite: { id: 4401 } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "7d145f032eb3b03b5ac5868aa3cecf3e002bb6e2", "public-token", new Set(["Deploy UI preview version"])); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.failingDetails).toEqual([]); - }); - - it("still fails when the NEWER duplicate-named check-run is the one that failed (recency-aware, not duplicate-blind)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { id: 1, name: "Deploy UI preview version", status: "completed", conclusion: "success", started_at: "2026-07-06T20:56:33Z", check_suite: { id: 4401 } }, - { id: 2, name: "Deploy UI preview version", status: "completed", conclusion: "failure", started_at: "2026-07-06T21:34:29Z", check_suite: { id: 4401 } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["Deploy UI preview version"])); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "Deploy UI preview version" })]); - }); - - it("keeps the already-latest entry when a stale duplicate is listed OUT OF ORDER (appears second but started EARLIER)", async () => { - // GitHub does not document a stable ordering contract for /check-runs, so the comparison must genuinely - // compare timestamps rather than assume "later in the array is newer" — this fixture puts the STALE - // (older, failing) run SECOND to prove the earlier-started duplicate does not override the real latest. - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { id: 2, name: "Deploy UI preview version", status: "completed", conclusion: "skipped", started_at: "2026-07-06T21:34:29Z", check_suite: { id: 4401 } }, - { id: 1, name: "Deploy UI preview version", status: "completed", conclusion: "failure", started_at: "2026-07-06T20:56:33Z", check_suite: { id: 4401 } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["Deploy UI preview version"])); - - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.failingDetails).toEqual([]); - }); - - it("does not discard failing same-name check-runs from a different suite", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { id: 1, name: "security", status: "completed", conclusion: "failure", started_at: "2026-07-06T20:56:33Z", app: { slug: "required-security-ci" }, check_suite: { id: 9001 } }, - { id: 2, name: "security", status: "completed", conclusion: "success", started_at: "2026-07-06T21:34:29Z", app: { slug: "colliding-helper-ci" }, check_suite: { id: 9002 } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["security"])); - - expect(aggregate.ciState).toBe("failed"); - expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "security" })]); - }); - - it("falls back to array order when neither duplicate has a started_at (queued runs have none)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/check-runs?")) { - return Response.json({ - check_runs: [ - { id: 1, name: "flaky", status: "completed", conclusion: "failure", started_at: null, check_suite: { id: 4401 } }, - { id: 2, name: "flaky", status: "completed", conclusion: "success", started_at: null, check_suite: { id: 4401 } }, - ], - }); - } - if (url.includes("/status?")) return Response.json({ statuses: [] }); - if (url.includes("/check-suites?")) return Response.json({ check_suites: [{ status: "completed", app: { slug: "github-actions" } }] }); - return new Response("not found", { status: 404 }); - }); - - const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["flaky"])); - - // No timestamp to compare on either side → the later array entry wins (the documented tiebreak fallback). - expect(aggregate.ciState).toBe("passed"); - expect(aggregate.failingDetails).toEqual([]); - }); - }); - }); - - describe("fetchLiveReviewThreadBlockers", () => { - it("returns unresolved non-outdated scanner review threads as blockers", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() === "https://api.github.com/graphql") { - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/signals/redaction.ts", - line: 30, - comments: { - nodes: [ - { - body: "\n**P1:** PUBLIC_LOCAL_PATH_INLINE regex fails to match Windows backslash paths", - url: "https://github.example/thread", - author: { login: "superagent-security[bot]" }, - }, - ], - }, - }, - ], - }, - }, - }, - }, - }); - } - return new Response("not found", { status: 404 }); - }); - - const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1748, "public-token"); - - expect(blockers).toEqual([ - expect.objectContaining({ - title: "PUBLIC_LOCAL_PATH_INLINE regex fails to match Windows backslash paths", - priority: "P1", - path: "src/signals/redaction.ts", - line: 30, - authorLogin: "superagent-security[bot]", - url: "https://github.example/thread", - scannerFinding: true, - }), - ]); - }); - - it("only trusts exact scanner bot logins for scanner-authored review thread blockers", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/superagent.ts", - line: 10, - comments: { nodes: [{ body: "**P1:** Canonical Superagent blocker", author: { login: "superagent[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/superagent-security.ts", - line: 20, - comments: { nodes: [{ body: "**P1:** Canonical Superagent Security blocker", author: { login: "superagent-security[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/superagent-security-dev.ts", - line: 30, - comments: { nodes: [{ body: "**P1:** Canonical Superagent Security Dev blocker", author: { login: "SUPERAGENT-SECURITY-DEV[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/brin.ts", - line: 40, - comments: { nodes: [{ body: "\n**P1:** Canonical Brin blocker", author: { login: "brin[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/superagentsecurity.ts", - line: 50, - comments: { nodes: [{ body: "**P1:** Typosquat without separator", author: { login: "superagentsecurity[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/superagent-evil.ts", - line: 60, - comments: { nodes: [{ body: "**P1:** Typosquat suffix", author: { login: "superagent-evil[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/brin-security.ts", - line: 70, - comments: { nodes: [{ body: "\n**P1:** Brin suffix typosquat", author: { login: "brin-security[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/missing-author.ts", - line: 80, - comments: { nodes: [{ body: "**P1:** Missing author cannot authorize", author: null }] }, - }, - ], - }, - }, - }, - }, - }); - }); - - const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); - - expect(blockers.map((blocker) => blocker.title)).toEqual([ - "Canonical Superagent blocker", - "Canonical Superagent Security blocker", - "Canonical Superagent Security Dev blocker", - "Canonical Brin blocker", - ]); - expect(blockers.map((blocker) => blocker.authorLogin)).toEqual(["superagent[bot]", "superagent-security[bot]", "SUPERAGENT-SECURITY-DEV[bot]", "brin[bot]"]); - expect(blockers.map((blocker) => blocker.path)).toEqual(["src/superagent.ts", "src/superagent-security.ts", "src/superagent-security-dev.ts", "src/brin.ts"]); - }); - - it("trusts self-host-configured TRUSTED_SCANNER_BOT_LOGINS additively alongside the built-in defaults (#4614)", async () => { - // Whitespace + case variation + an empty entry between commas -- exercises the trim/lowercase/filter - // handling, not just a bare exact match. - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token", TRUSTED_SCANNER_BOT_LOGINS: " CodeQL[bot] ,,Snyk-Security[bot]" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/codeql-finding.ts", - line: 5, - comments: { nodes: [{ body: "**P1:** Configured CodeQL blocker", author: { login: "codeql[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/snyk-finding.ts", - line: 15, - comments: { nodes: [{ body: "**P1:** Configured Snyk blocker", author: { login: "snyk-security[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/superagent-still-trusted.ts", - line: 25, - comments: { nodes: [{ body: "**P1:** Built-in default still trusted", author: { login: "superagent-security[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/unconfigured-scanner.ts", - line: 35, - comments: { nodes: [{ body: "**P1:** Unconfigured scanner stays untrusted", author: { login: "semgrep[bot]" }, authorAssociation: "NONE" }] }, - }, - ], - }, - }, - }, - }, - }); - }); - - const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1900, "public-token"); - - expect(blockers.map((blocker) => blocker.title)).toEqual(["Configured CodeQL blocker", "Configured Snyk blocker", "Built-in default still trusted"]); - expect(blockers.map((blocker) => blocker.authorLogin)).toEqual(["codeql[bot]", "snyk-security[bot]", "superagent-security[bot]"]); - }); - - it("ignores a whitespace-only TRUSTED_SCANNER_BOT_LOGINS override and keeps only the built-in defaults trusted", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token", TRUSTED_SCANNER_BOT_LOGINS: " " }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/codeql-finding.ts", - line: 5, - comments: { nodes: [{ body: "**P1:** Not configured, must not block", author: { login: "codeql[bot]" }, authorAssociation: "NONE" }] }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/superagent-still-trusted.ts", - line: 25, - comments: { nodes: [{ body: "**P1:** Built-in default still trusted", author: { login: "superagent-security[bot]" }, authorAssociation: "NONE" }] }, - }, - ], - }, - }, - }, - }, - }); - }); - - const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1901, "public-token"); - - expect(blockers.map((blocker) => blocker.authorLogin)).toEqual(["superagent-security[bot]"]); - }); - - it("paginates review threads so blockers beyond the first page cannot hide", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const queries: string[] = []; - const fetchSpy = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { - if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - const query = JSON.parse(String(init?.body)).query as string; - queries.push(query); - if (!query.includes("after:")) { - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [{ isResolved: true, isOutdated: false, path: "resolved.ts", line: 1, comments: { nodes: [{ body: "already resolved", author: { login: "superagent-security[bot]" } }] } }], - pageInfo: { hasNextPage: true, endCursor: "cursor-1" }, - }, - }, - }, - }, - }); - } - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/hidden.ts", - line: 77, - comments: { - nodes: [ - { - body: "**P0:** Hidden second-page review thread must block", - url: "https://github.example/thread/second-page", - author: { login: "superagent-security[bot]" }, - }, - ], - }, - }, - ], - pageInfo: { hasNextPage: false, endCursor: "cursor-2" }, - }, - }, - }, - }, - }); - }); - vi.stubGlobal("fetch", fetchSpy); - - const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); - - expect(fetchSpy).toHaveBeenCalledTimes(2); - expect(queries[0]).toContain("reviewThreads(first: 50)"); - expect(queries[1]).toContain('reviewThreads(first: 50, after: "cursor-1")'); - expect(blockers).toEqual([ - expect.objectContaining({ - title: "Hidden second-page review thread must block", - priority: "P0", - path: "src/hidden.ts", - line: 77, - url: "https://github.example/thread/second-page", - }), - ]); - }); - - it("stops review-thread pagination on a repeated cursor without dropping fetched blockers", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - let calls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - calls += 1; - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: - calls === 1 - ? [] - : [ - { - isResolved: false, - isOutdated: false, - path: "src/repeated-cursor.ts", - line: 9, - comments: { nodes: [{ body: "**P1:** Repeated cursor blocker", author: { login: "superagent-security[bot]" } }] }, - }, - ], - pageInfo: { hasNextPage: true, endCursor: "cursor-1" }, - }, - }, - }, - }, - }); - }); - - const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); - - expect(calls).toBe(2); - expect(blockers).toEqual([ - expect.objectContaining({ - title: "Repeated cursor blocker", - path: "src/repeated-cursor.ts", - line: 9, - }), - ]); - }); - - it("keeps fetched review-thread blockers when a later page is malformed", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - let calls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - calls += 1; - if (calls === 2) { - return Response.json({ data: { repository: { pullRequest: { reviewThreads: null } } } }); - } - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/fetched-before-malformed-page.ts", - line: 14, - comments: { nodes: [{ body: "**P1:** Fetched blocker before malformed page", author: { login: "superagent-security[bot]" } }] }, - }, - ], - pageInfo: { hasNextPage: true, endCursor: "cursor-1" }, - }, - }, - }, - }, - }); - }); - - const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); - - expect(calls).toBe(2); - expect(blockers).toEqual([ - expect.objectContaining({ - title: "Fetched blocker before malformed page", - path: "src/fetched-before-malformed-page.ts", - line: 14, - }), - ]); - }); - - it("stops review-thread pagination when GitHub omits the next cursor", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const fetchSpy = vi.fn(async (input: RequestInfo | URL) => { - if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/missing-cursor.ts", - line: 12, - comments: { nodes: [{ body: "**P2:** Missing cursor blocker", author: { login: "superagent-security[bot]" } }] }, - }, - ], - pageInfo: { hasNextPage: true, endCursor: null }, - }, - }, - }, - }, - }); - }); - vi.stubGlobal("fetch", fetchSpy); - - const blockers = await fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token"); - - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(blockers).toEqual([ - expect.objectContaining({ - title: "Missing cursor blocker", - path: "src/missing-cursor.ts", - line: 12, - }), - ]); - }); - - it("ignores unresolved review threads from untrusted public commenters", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/security.ts", - line: 42, - comments: { - nodes: [ - { - body: "\n**P0:** Forged public blocker", - url: "https://github.example/thread/untrusted", - author: { login: "random-outsider" }, - authorAssociation: "NONE", - }, - ], - }, - }, - ], - }, - }, - }, - }, - }); - }); - - await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token")).resolves.toEqual([]); - }); - - it("verifies member review thread authors against live repository permissions", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const permissionRequests: string[] = []; - const permissionUrl = (login: string) => `https://api.github.com/repos/JSONbored/gittensory/collaborators/${login}/permission`; - const permissionResponses = new Map Response>([ - [permissionUrl("repo-maintainer"), () => Response.json({ permission: "maintain" })], - [permissionUrl("repo-admin"), () => Response.json({ permission: "admin" })], - [permissionUrl("repo-writer"), () => Response.json({ permission: "write" })], - [permissionUrl("org-member"), () => Response.json({ permission: "read" })], - [permissionUrl("member-lookup-fails"), () => new Response("permission unavailable", { status: 403 })], - ]); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - const permissionResponse = permissionResponses.get(url); - if (permissionResponse) { - permissionRequests.push(url); - return permissionResponse(); - } - if (url !== "https://api.github.com/graphql") return new Response("not found", { status: 404 }); - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { - isResolved: false, - isOutdated: false, - path: "src/maintainer-owner.ts", - line: 7, - comments: { - nodes: [ - { - body: "Owner requested change", - url: "https://github.example/thread/owner", - author: { login: "repo-owner" }, - authorAssociation: "OWNER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/maintainer-member.ts", - line: 8, - comments: { - nodes: [ - { - body: "Maintainer requested change", - url: "https://github.example/thread/maintainer", - author: { login: "repo-maintainer" }, - authorAssociation: "MEMBER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/maintainer-collaborator.ts", - line: 9, - comments: { - nodes: [ - { - body: "Collaborator requested change", - url: "https://github.example/thread/collaborator", - author: { login: "repo-collaborator" }, - authorAssociation: "COLLABORATOR", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/scanner.ts", - line: 10, - comments: { - nodes: [ - { - body: "Scanner requested change", - url: "https://github.example/thread/scanner", - author: { login: "superagent-security[bot]" }, - authorAssociation: "NONE", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/admin-member.ts", - line: 11, - comments: { - nodes: [ - { - body: "Admin requested change", - url: "https://github.example/thread/admin", - author: { login: "repo-admin" }, - authorAssociation: "MEMBER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/writer-member.ts", - line: 12, - comments: { - nodes: [ - { - body: "Writer requested change", - url: "https://github.example/thread/writer", - author: { login: "repo-writer" }, - authorAssociation: "MEMBER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/own-member.ts", - line: 13, - comments: { - nodes: [ - { - body: "Own bot requested change", - url: "https://github.example/thread/own-member", - author: { login: "gittensory-orb[bot]" }, - authorAssociation: "MEMBER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/maintainer-member-repeat.ts", - line: 14, - comments: { - nodes: [ - { - body: "Maintainer repeated change", - url: "https://github.example/thread/maintainer-repeat", - author: { login: "repo-maintainer" }, - authorAssociation: "MEMBER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/org-member.ts", - line: 15, - comments: { - nodes: [ - { - body: "Org member requested change", - url: "https://github.example/thread/org-member", - author: { login: "org-member" }, - authorAssociation: "MEMBER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/member-lookup-fails.ts", - line: 16, - comments: { - nodes: [ - { - body: "Unverified member requested change", - url: "https://github.example/thread/member-lookup-fails", - author: { login: "member-lookup-fails" }, - authorAssociation: "MEMBER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/member-missing-author.ts", - line: 17, - comments: { - nodes: [ - { - body: "Member association with missing author", - url: "https://github.example/thread/member-missing-author", - author: null, - authorAssociation: "MEMBER", - }, - ], - }, - }, - { - isResolved: false, - isOutdated: false, - path: "src/member-blank-author.ts", - line: 18, - comments: { - nodes: [ - { - body: "Member association with blank author", - url: "https://github.example/thread/member-blank-author", - author: { login: " " }, - authorAssociation: "MEMBER", - }, - ], - }, - }, - ], - }, - }, - }, - }, - }); - }); - - await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1781, "public-token")).resolves.toEqual([ - expect.objectContaining({ - title: "Owner requested change", - authorLogin: "repo-owner", - scannerFinding: false, - }), - expect.objectContaining({ - title: "Maintainer requested change", - authorLogin: "repo-maintainer", - scannerFinding: false, - }), - expect.objectContaining({ - title: "Collaborator requested change", - authorLogin: "repo-collaborator", - scannerFinding: false, - }), - expect.objectContaining({ - title: "Scanner requested change", - authorLogin: "superagent-security[bot]", - scannerFinding: false, - }), - expect.objectContaining({ - title: "Admin requested change", - authorLogin: "repo-admin", - scannerFinding: false, - }), - expect.objectContaining({ - title: "Writer requested change", - authorLogin: "repo-writer", - scannerFinding: false, - }), - expect.objectContaining({ - title: "Maintainer repeated change", - authorLogin: "repo-maintainer", - scannerFinding: false, - }), - ]); - expect(permissionRequests).toEqual([ - permissionUrl("repo-maintainer"), - permissionUrl("repo-admin"), - permissionUrl("repo-writer"), - permissionUrl("org-member"), - permissionUrl("member-lookup-fails"), - ]); - }); - - it("ignores resolved, outdated, own-bot, and empty review threads", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() === "https://api.github.com/graphql") { - return Response.json({ - data: { - repository: { - pullRequest: { - reviewThreads: { - nodes: [ - { isResolved: true, isOutdated: false, path: "a.ts", line: 1, comments: { nodes: [{ body: "resolved", author: { login: "superagent-security[bot]" } }] } }, - { isResolved: false, isOutdated: true, path: "b.ts", line: 2, comments: { nodes: [{ body: "outdated", author: { login: "superagent-security[bot]" } }] } }, - { isResolved: false, isOutdated: false, path: "c.ts", line: 3, comments: { nodes: [{ body: "own bot", author: { login: "gittensory-orb[bot]" }, authorAssociation: "OWNER" }] } }, - { isResolved: false, isOutdated: false, path: "own-collaborator.ts", line: 5, comments: { nodes: [{ body: "own bot with collaborator association", author: { login: "gittensory[bot]" }, authorAssociation: "COLLABORATOR" }] } }, - { isResolved: false, isOutdated: false, path: "no-comments.ts", line: 6, comments: null }, - { isResolved: false, isOutdated: false, path: "d.ts", line: 4, comments: { nodes: [{ body: " ", author: { login: "superagent-security[bot]" } }, null] } }, - null, - ], - }, - }, - }, - }, - }); - } - return new Response("not found", { status: 404 }); - }); - - await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1, "public-token")).resolves.toEqual([]); - }); - - it("fails open without a token, malformed repo name, or GraphQL response", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const fetchSpy = vi.fn(async () => new Response("boom", { status: 500 })); - vi.stubGlobal("fetch", fetchSpy); - - await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1, undefined)).resolves.toEqual([]); - expect(fetchSpy).not.toHaveBeenCalled(); - await expect(fetchLiveReviewThreadBlockers(env, "malformed", 1, "public-token")).resolves.toEqual([]); - expect(fetchSpy).not.toHaveBeenCalled(); - await expect(fetchLiveReviewThreadBlockers(env, "JSONbored/gittensory", 1, "public-token")).resolves.toEqual([]); - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe("mergeRequiredCiContexts", () => { - it("unions branch-protection contexts with expectedCiContexts when both have entries", () => { - const merged = mergeRequiredCiContexts(new Set(["build"]), ["test", "lint"]); - expect([...(merged as Set)].sort()).toEqual(["build", "lint", "test"]); - }); - - it("returns branch-protection contexts unchanged when expectedCiContexts is undefined", () => { - const merged = mergeRequiredCiContexts(new Set(["build", "test"]), undefined); - expect(merged).toBeInstanceOf(Set); - expect([...(merged as Set)].sort()).toEqual(["build", "test"]); - }); - - it("returns branch-protection contexts unchanged when expectedCiContexts is an empty array", () => { - const merged = mergeRequiredCiContexts(new Set(["build", "test"]), []); - expect([...(merged as Set)].sort()).toEqual(["build", "test"]); - }); - - it("returns branch-protection contexts unchanged when expectedCiContexts is null", () => { - const merged = mergeRequiredCiContexts(new Set(["build", "test"]), null); - expect([...(merged as Set)].sort()).toEqual(["build", "test"]); - }); - - it("returns just the expected set when branch protection is null and expectedCiContexts has entries", () => { - const merged = mergeRequiredCiContexts(null, ["build"]); - expect([...(merged as Set)]).toEqual(["build"]); - }); - - it("returns null when branch protection is null and expectedCiContexts is undefined", () => { - expect(mergeRequiredCiContexts(null, undefined)).toBeNull(); - }); - - it("returns null when branch protection is null and expectedCiContexts is null", () => { - expect(mergeRequiredCiContexts(null, null)).toBeNull(); - }); - - it("returns null when branch protection is null and expectedCiContexts is an empty array", () => { - expect(mergeRequiredCiContexts(null, [])).toBeNull(); - }); - - it("returns just the expected set when branch protection is an empty (non-null) Set and expectedCiContexts has entries", () => { - const merged = mergeRequiredCiContexts(new Set(), ["build"]); - expect([...(merged as Set)]).toEqual(["build"]); - }); - - it("drops blank/whitespace-only expectedCiContexts entries while keeping real entries", () => { - const merged = mergeRequiredCiContexts(null, [" ", "", "build"]); - expect([...(merged as Set)]).toEqual(["build"]); - }); - - it("trims leading/trailing whitespace from expectedCiContexts entries in the result", () => { - const merged = mergeRequiredCiContexts(null, [" build "]); - expect([...(merged as Set)]).toEqual(["build"]); - }); - }); - - describe("fetchRequiredStatusContexts", () => { - it("returns null without fetching when baseRef is missing", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const fetchSpy = vi.fn(); - vi.stubGlobal("fetch", fetchSpy); - expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", null, "public-token")).toBeNull(); - expect(fetchSpy).not.toHaveBeenCalled(); - }); - - it("returns the live required set when branch protection is readable (both contexts and checks shapes)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/protection/required_status_checks")) { - return Response.json({ contexts: ["validate", "", null], checks: [{ context: "Superagent Security Scan" }, { context: " " }] }); - } - return new Response("not found", { status: 404 }); - }); - const required = await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token"); - expect([...(required as Set)].sort()).toEqual(["Superagent Security Scan", "validate"]); - }); - - it("uses the shared GitHub GET cache for raw branch-protection reads without double-counting rate-limit observations", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const store = new Map(); - setGitHubResponseCache({ - get: async (key) => store.get(key) ?? null, - set: async (key, value) => void store.set(key, value), - }); - let fetches = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - fetches += 1; - expect(input.toString()).toContain("/branches/main/protection/required_status_checks"); - return Response.json( - { contexts: ["validate"], checks: [] }, - { headers: { "x-ratelimit-limit": "5000", "x-ratelimit-remaining": "4999", "x-ratelimit-reset": "1782802800" } }, - ); - }); - - // admissionKey mirrors the real caller (processors.ts), which always resolves + passes its own admission - // key -- omitting it here would exercise the (deliberately unpersisted) no-attribution path instead of - // this test's actual point: that a cache hit does not double-record telemetry for the SAME bucket. - const admissionKey = githubRateLimitAdmissionKeyForPublicToken(); - const first = await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token", admissionKey); - const second = await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token", admissionKey); - - expect([...(first as Set)]).toEqual(["validate"]); - expect([...(second as Set)]).toEqual(["validate"]); - expect(fetches).toBe(1); - expect([...store.keys()].some((key) => key.includes("/branches/main/protection/required_status_checks"))).toBe(true); - const observations = await listLatestGitHubRateLimitObservations(env); - expect(observations).toHaveLength(1); - expect(observations[0]).toMatchObject({ - repoFullName: "JSONbored/gittensory", - resource: "rest", - path: "/branches/main/protection/required_status_checks", - statusCode: 200, - remaining: 4999, - admissionKey, - }); - }); - - it("returns null when the live read fails, even if a stale global fallback is configured (conservative fold-all)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - (env as Env & { GITTENSORY_REQUIRED_CI_CONTEXTS?: string }).GITTENSORY_REQUIRED_CI_CONTEXTS = "stale-required-context"; - vi.stubGlobal("fetch", async () => new Response("forbidden", { status: 403 })); - expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); - }); - - it("classifies a bare 403 (no admin:read) as permission-denied, not a rate limit (#selfhost-runtime-pressure)", async () => { - resetMetrics(); - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => new Response("forbidden", { status: 403 })); - expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); - expect(await renderMetrics()).toContain("gittensory_github_branch_protection_permission_denied_total 1"); - }); - - it("does not count a 404 (no branch protection configured) as permission-denied", async () => { - resetMetrics(); - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); - expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); - expect(await renderMetrics()).not.toContain("gittensory_github_branch_protection_permission_denied_total"); - }); - - it("does not count a genuinely rate-limited 403 (x-ratelimit-remaining: 0) as permission-denied", async () => { - resetMetrics(); - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal( - "fetch", - async () => - new Response("secondary rate limit", { - status: 403, - headers: { "x-ratelimit-remaining": "0", "x-ratelimit-reset": "1780000000" }, - }), - ); - expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); - expect(await renderMetrics()).not.toContain("gittensory_github_branch_protection_permission_denied_total"); - }, 15_000); - }); - - describe("fetchNamedCheckRunConclusion (#2564)", () => { - it("returns undefined without fetching when headSha is missing", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const fetchSpy = vi.fn(); - vi.stubGlobal("fetch", fetchSpy); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", null, "CLA Assistant Lite", "cla-assistant", "public-token")).toBeUndefined(); - expect(fetchSpy).not.toHaveBeenCalled(); - }); - - it("returns the lowercased conclusion for a matching check-run (case-insensitive name match)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - expect(input.toString()).toContain("/commits/sha1/check-runs"); - return Response.json({ total_count: 1, check_runs: [{ id: 1, name: "cla assistant lite", status: "completed", conclusion: "SUCCESS", app: { slug: "cla-assistant" } }] }); - }); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBe("success"); - }); - - it("REGRESSION (gate finding): returns null (deterministic missing), not undefined (transient), without fetching when no trusted app slug is configured — a check-run-only config with no slug must still BLOCK, not silently hold forever", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - const fetchSpy = vi.fn(); - vi.stubGlobal("fetch", fetchSpy); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", null, "public-token")).toBeNull(); - expect(fetchSpy).not.toHaveBeenCalled(); - }); - - it("ignores a completed same-name check-run from an untrusted app slug", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => - Response.json({ - total_count: 1, - check_runs: [{ id: 1, name: "CLA Assistant Lite", status: "completed", conclusion: "success", app: { slug: "github-actions" } }], - }), - ); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeNull(); - }); - - it("uses the trusted producer when spoofed and trusted same-name runs both exist", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => - Response.json({ - total_count: 2, - check_runs: [ - { id: 1, name: "CLA Assistant Lite", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { id: 2, name: "CLA Assistant Lite", status: "completed", conclusion: "failure", app: { slug: "cla-assistant" } }, - ], - }), - ); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBe("failure"); - }); - - it("returns null (resolved: not found) when the head SHA has no check-run with that name", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => Response.json({ total_count: 1, check_runs: [{ id: 1, name: "Some Other Check", status: "completed", conclusion: "success", app: { slug: "cla-assistant" } }] })); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeNull(); - }); - - it("returns null (resolved: not found) when the response omits check_runs entirely (nullish fallback)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => Response.json({ total_count: 0 })); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeNull(); - }); - - // #2564 gate-review finding: a matching check-run that has NOT finished yet must resolve to undefined - // (unresolved), not "" — an in-progress run's conclusion:null means "not decided yet," not "resolved with - // an empty conclusion." Coercing it to "" made claMode: block hard-fail a PR before the named check had - // actually finished running. - it("returns undefined (unresolved) for a matching but still-in-progress check-run (status !== completed, conclusion: null)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => Response.json({ total_count: 1, check_runs: [{ id: 1, name: "CLA Assistant Lite", status: "in_progress", conclusion: null, app: { slug: "cla-assistant" } }] })); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeUndefined(); - }); - - it("returns an empty string for a matching, COMPLETED check-run with an unexpected empty conclusion (genuine edge case, not the in-progress case)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => Response.json({ total_count: 1, check_runs: [{ id: 1, name: "CLA Assistant Lite", status: "completed", conclusion: null, app: { slug: "cla-assistant" } }] })); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBe(""); - }); - - it("returns undefined (not evaluated) when the fetch fails, never a false 'missing'", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - vi.stubGlobal("fetch", async () => new Response("forbidden", { status: 403 })); - expect(await fetchNamedCheckRunConclusion(env, "JSONbored/gittensory", "sha1", "CLA Assistant Lite", "cla-assistant", "public-token")).toBeUndefined(); - }); - }); - - describe("fetchLinkedIssueFacts (#2136)", () => { - it("returns a found result with the extracted facts, falling back to the requested number and open state when the payload omits them", async () => { - const env = createTestEnv({}); - // Sparse payload: no `number`, no `state` — exercises the `data.number ?? issueNumber` and - // `data.state ?? "open"` defensive fallbacks. - vi.stubGlobal("fetch", async () => Response.json({ labels: [{ name: "bug" }, "manual-string-label"], assignees: [{ login: "maintainer" }], user: { login: "reporter" } })); - const result = await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 42, "tok"); - expect(result).toEqual({ - status: "found", - facts: { number: 42, labels: ["bug", "manual-string-label"], assignees: ["maintainer"], state: "open", authorLogin: "reporter", title: null, body: null, closedAt: null }, - }); - }); - - it("extracts title + body (#1961/#3906, linked-issue satisfaction assessment) from the same REST payload — no second fetch", async () => { - const env = createTestEnv({}); - vi.stubGlobal("fetch", async () => - Response.json({ - number: 1275, - state: "open", - labels: [], - assignees: [], - user: { login: "reporter" }, - title: "Enrich SN74 Gittensor — add SSE stream", - body: "We need a live SSE stream surface for SN74 Gittensor.", - }), - ); - const result = await fetchLinkedIssueFacts(env, "JSONbored/metagraphed", 1275, "tok"); - expect(result).toEqual({ - status: "found", - facts: { - number: 1275, - labels: [], - assignees: [], - state: "open", - authorLogin: "reporter", - title: "Enrich SN74 Gittensor — add SSE stream", - body: "We need a live SSE stream surface for SN74 Gittensor.", - closedAt: null, - }, - }); - }); - - it("extracts closedAt (#4528) from the same REST payload when the issue is closed", async () => { - const env = createTestEnv({}); - vi.stubGlobal("fetch", async () => - Response.json({ number: 4279, state: "closed", closed_at: "2026-07-09T22:15:14Z" }), - ); - const result = await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 4279, "tok"); - expect(result.status === "found" && result.facts.closedAt).toBe("2026-07-09T22:15:14Z"); - }); - - it("falls back to null for closedAt (#4528) when the payload omits it or it isn't a string", async () => { - const env = createTestEnv({}); - vi.stubGlobal("fetch", async () => Response.json({ number: 4279, state: "open", closed_at: null })); - const result = await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 4279, "tok"); - expect(result.status === "found" && result.facts.closedAt).toBeNull(); - }); - - it("falls back to null for title/body when the payload omits them or they are empty strings", async () => { - const env = createTestEnv({}); - vi.stubGlobal("fetch", async () => Response.json({ number: 7, state: "open", title: "", body: "" })); - const result = await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 7, "tok"); - expect(result.status).toBe("found"); - expect(result.status === "found" && result.facts.title).toBeNull(); - expect(result.status === "found" && result.facts.body).toBeNull(); - }); - - it("returns not_found on a confirmed 404, distinct from a transient fetch error", async () => { - const env = createTestEnv({}); - vi.stubGlobal("fetch", async () => new Response("missing", { status: 404 })); - expect(await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 999999, "tok")).toEqual({ status: "not_found" }); - }); - - it("REGRESSION: treats a 404 seen with the public/anonymous token as fetch_error, not not_found — GitHub also returns 404 for a real but inaccessible private issue", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-tok" }); - vi.stubGlobal("fetch", async () => new Response("missing", { status: 404 })); - // The public token proves nothing about repo access, so a 404 here could just as easily mean "this issue - // is real but private and this token can't see it" -- treating it as CONFIRMED absence risks closing a PR - // over a genuinely-linked issue. - expect(await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 42, env.GITHUB_PUBLIC_TOKEN)).toEqual({ status: "fetch_error" }); - }); - - it("REGRESSION: treats a 404 seen with no token at all as fetch_error, not not_found", async () => { - const env = createTestEnv({}); - vi.stubGlobal("fetch", async () => new Response("missing", { status: 404 })); - expect(await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 42, undefined)).toEqual({ status: "fetch_error" }); - }); - - it("returns fetch_error on a transient failure (5xx), never conflating it with not_found", async () => { - const env = createTestEnv({}); - vi.stubGlobal("fetch", async () => new Response("server error", { status: 500 })); - expect(await fetchLinkedIssueFacts(env, "JSONbored/gittensory", 42, "tok")).toEqual({ status: "fetch_error" }); - }); - }); - - describe("isRateLimitedGitHubFailure", () => { - it("does not treat a bare permission 403 (remaining > 0, no Retry-After, no secondary body) as a rate limit", () => { - expect( - isRateLimitedGitHubFailure({ statusCode: 403, retryAfter: null, remaining: "4999", body: "Resource not accessible by integration" }), - ).toBe(false); - }); - - it("treats a 403 with an exhausted x-ratelimit-remaining as a rate limit", () => { - expect(isRateLimitedGitHubFailure({ statusCode: 403, retryAfter: null, remaining: "0", body: "" })).toBe(true); - }); - - it("treats a 403 or 429 carrying a Retry-After header as a rate limit", () => { - expect(isRateLimitedGitHubFailure({ statusCode: 403, retryAfter: "60", remaining: "100", body: "" })).toBe(true); - expect(isRateLimitedGitHubFailure({ statusCode: 429, retryAfter: "1", remaining: null, body: "" })).toBe(true); - }); - - it("treats a secondary-limit / abuse body as a rate limit", () => { - expect( - isRateLimitedGitHubFailure({ statusCode: 403, retryAfter: null, remaining: "100", body: "You have exceeded a secondary rate limit" }), - ).toBe(true); - }); - - it("does not treat a 429 without any rate-limit signal as a rate limit", () => { - expect(isRateLimitedGitHubFailure({ statusCode: 429, retryAfter: null, remaining: "100", body: "" })).toBe(false); - }); - - it("does not treat a non-403/429 failure as a rate limit even with a matching body", () => { - expect(isRateLimitedGitHubFailure({ statusCode: 500, retryAfter: null, remaining: null, body: "secondary rate limit" })).toBe(false); - }); - }); - - describe("reconcileOpenPullRequests (#audit-open-pr-reconciliation)", () => { - it("returns an all-zero result for a repo that does not exist", async () => { - const env = createTestEnv(); - expect(await reconcileOpenPullRequests(env, "owner/missing")).toEqual({ repoFullName: "owner/missing", remoteOpenCount: 0, localOpenCount: 0, missingNumbers: [] }); - }); - - it("reports no missing numbers when the local table already has every remote-open PR", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - await seedRegisteredRepo(env); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/pulls?")) return Response.json([{ number: 1 }]); - return Response.json([]); - }); - - const result = await reconcileOpenPullRequests(env, "JSONbored/gittensory"); - - expect(result).toEqual({ repoFullName: "JSONbored/gittensory", remoteOpenCount: 1, localOpenCount: 1, missingNumbers: [] }); - }); - - it("REGRESSION (#3782/#3793): reports a PR number GitHub has open that has no local row at all", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - await seedRegisteredRepo(env); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/pulls?")) return Response.json([{ number: 1 }, { number: 7 }]); // #7 opened but never made it into the local table - return Response.json([]); - }); - - const result = await reconcileOpenPullRequests(env, "JSONbored/gittensory"); - - expect(result).toEqual({ repoFullName: "JSONbored/gittensory", remoteOpenCount: 2, localOpenCount: 1, missingNumbers: [7] }); - }); - - it("paginates the open-PR list past the first 100 via the Link header", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - await seedRegisteredRepo(env); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/pulls?")) { - const page = Number(new URL(url).searchParams.get("page") ?? "1"); - if (page === 1) { - return Response.json( - Array.from({ length: 100 }, (_, i) => ({ number: i + 1 })), - { headers: { link: '; rel="next"' } }, - ); - } - return Response.json([{ number: 101 }]); - } - return Response.json([]); - }); - - const result = await reconcileOpenPullRequests(env, "JSONbored/gittensory"); - - expect(result.remoteOpenCount).toBe(101); - expect(result.missingNumbers).toEqual(expect.arrayContaining([1, 101])); - }); - - it("fails open (all-zero result) when the FIRST page fails, so a GitHub hiccup never falsely reports every local PR as missing", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - await seedRegisteredRepo(env); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); - vi.stubGlobal("fetch", async () => new Response("down", { status: 500 })); - - expect(await reconcileOpenPullRequests(env, "JSONbored/gittensory")).toEqual({ repoFullName: "JSONbored/gittensory", remoteOpenCount: 0, localOpenCount: 0, missingNumbers: [] }); - }); - - it("keeps the pages already fetched when a LATER page fails mid-crawl (a partial remote list can only under-report, never falsely flag a real local PR)", async () => { - const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); - await seedRegisteredRepo(env); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/pulls?")) { - const page = Number(new URL(url).searchParams.get("page") ?? "1"); - if (page === 1) { - return Response.json( - Array.from({ length: 100 }, (_, i) => ({ number: i + 1 })), - { headers: { link: '; rel="next"' } }, - ); - } - return new Response("down", { status: 500 }); - } - return Response.json([]); - }); - - const result = await reconcileOpenPullRequests(env, "JSONbored/gittensory"); - - expect(result.remoteOpenCount).toBe(100); // page 1's 100 items are kept despite page 2 failing - }); - }); - -}); - -async function seedRegisteredRepo(env: Env) { - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { - "JSONbored/gittensory": { - emission_share: 0.01, - issue_discovery_share: 0, - trusted_label_pipeline: true, - label_multipliers: { bug: 1.1, refactor: 0.5 }, - }, - }, - { kind: "raw-github", url: "https://example.test/master_repositories.json" }, - "2026-05-23T00:00:00.000Z", - ), - ); -} - -async function generatePrivateKeyPem(): Promise { - const key = (await crypto.subtle.generateKey( - { - name: "RSASSA-PKCS1-v1_5", - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }, - true, - ["sign", "verify"], - )) as CryptoKeyPair; - const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); - const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); - return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; -} - -async function persistTotalsSnapshot( - env: Env, - overrides: { - fetchedAt?: string; - sourceKind?: "github" | "installation"; - openIssuesTotal?: number; - openPullRequestsTotal?: number; - mergedPullRequestsTotal?: number; - closedUnmergedPullRequestsTotal?: number; - labelsTotal?: number; - } = {}, -) { - await persistRepoGithubTotalsSnapshot(env, { - id: crypto.randomUUID(), - repoFullName: "JSONbored/gittensory", - openIssuesTotal: overrides.openIssuesTotal ?? 0, - openPullRequestsTotal: overrides.openPullRequestsTotal ?? 0, - mergedPullRequestsTotal: overrides.mergedPullRequestsTotal ?? 0, - closedUnmergedPullRequestsTotal: overrides.closedUnmergedPullRequestsTotal ?? 0, - labelsTotal: overrides.labelsTotal ?? 0, - sourceKind: overrides.sourceKind ?? "github", - fetchedAt: overrides.fetchedAt ?? "2026-05-25T00:00:00.000Z", - payload: {}, - }); -} - -function githubTotalsResponse(counts: { openIssues: number; openPullRequests: number; mergedPullRequests: number; closedPullRequests: number; labels: number }) { - return Response.json({ - data: { - rateLimit: { remaining: 4999, resetAt: "2026-05-25T01:00:00.000Z" }, - repository: { - issues: { totalCount: counts.openIssues }, - openPullRequests: { totalCount: counts.openPullRequests }, - mergedPullRequests: { totalCount: counts.mergedPullRequests }, - closedPullRequests: { totalCount: counts.closedPullRequests }, - labels: { totalCount: counts.labels }, - }, - }, - }); -} - -describe("isOwnReviewThreadAuthor", () => { - const env = createTestEnv(); // GITHUB_APP_SLUG defaults to "gittensory" (test/helpers/d1.ts) - - it("matches our own gittensory app bot logins by prefix", () => { - for (const login of ["gittensory[bot]", "gittensory-orb[bot]", "gittensory-review[bot]", "GITTENSORY[bot]", "gittensory", "gittensory-orb"]) { - expect(isOwnReviewThreadAuthor(env, login)).toBe(true); - } - }); - - it("does not match a third-party bot whose slug only ends in -gittensory[bot] (regression)", () => { - // A `\b` boundary also fires after a hyphen, so the unanchored regex misclassified these external bots as - // our own author and dropped their review-thread comments as self-authored non-blockers (fail-open). - for (const login of ["evil-gittensory[bot]", "x-gittensory[bot]", "not-gittensory", "gittensory-fork"]) { - expect(isOwnReviewThreadAuthor(env, login)).toBe(false); - } - }); - - it("treats an absent login as not our own author", () => { - expect(isOwnReviewThreadAuthor(env, null)).toBe(false); - expect(isOwnReviewThreadAuthor(env, undefined)).toBe(false); - expect(isOwnReviewThreadAuthor(env, "")).toBe(false); - }); - - it("derives the match from GITHUB_APP_SLUG (#4615), not a hardcoded literal", () => { - const renamed = createTestEnv({ GITHUB_APP_SLUG: "acme-review" }); - expect(isOwnReviewThreadAuthor(renamed, "acme-review[bot]")).toBe(true); - expect(isOwnReviewThreadAuthor(renamed, "acme-review-orb[bot]")).toBe(true); - expect(isOwnReviewThreadAuthor(renamed, "acme-review")).toBe(true); - // The OLD slug no longer matches once an operator renames their App -- proves the literal is gone. - expect(isOwnReviewThreadAuthor(renamed, "gittensory[bot]")).toBe(false); - }); - - it("a slug containing regex metacharacters is escaped, not interpreted (defensive)", () => { - const weird = createTestEnv({ GITHUB_APP_SLUG: "acme.bot" }); - expect(isOwnReviewThreadAuthor(weird, "acme.bot[bot]")).toBe(true); - expect(isOwnReviewThreadAuthor(weird, "acmexbot[bot]")).toBe(false); // "." must not act as a wildcard - }); - - it("fails closed when GITHUB_APP_SLUG is blank (misconfiguration)", () => { - const blank = createTestEnv({ GITHUB_APP_SLUG: "" }); - expect(isOwnReviewThreadAuthor(blank, "gittensory[bot]")).toBe(false); - expect(isOwnReviewThreadAuthor(blank, "")).toBe(false); - }); }); diff --git a/test/unit/queue-2.test.ts b/test/unit/queue-2.test.ts new file mode 100644 index 0000000000..3bd6c03541 --- /dev/null +++ b/test/unit/queue-2.test.ts @@ -0,0 +1,5352 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { generateKeyPairSync } from "node:crypto"; +import { clearInstallationTokenCacheForTest } from "../../src/github/app"; +import { clearReviewSuppressionCacheForTest } from "../../src/review/review-memory-wire"; +import { PR_PANEL_COMMENT_MARKER } from "../../src/github/comments"; +import * as backfillModule from "../../src/github/backfill"; +import * as rateLimitModule from "../../src/github/rate-limit"; +import * as repositoriesModule from "../../src/db/repositories"; +import * as reviewEffortModule from "../../src/review/review-effort"; +import * as repositorySettingsModule from "../../src/settings/repository-settings"; +import * as sentryModule from "../../src/selfhost/sentry"; +import { renderMetrics, resetMetrics } from "../../src/selfhost/metrics"; +import { jobCoalesceKey } from "../../src/selfhost/queue-common"; +import { + listCollisionEdges, + createAgentRun, + getCommandUsefulnessSummary, + getBurdenForecast, + getContributorEvidence, + getAgentRun, + getContributorScoringProfile, + getWebhookEvent, + getInstallation, + getLatestUpstreamRulesetSnapshot, + getPullRequest, + getPullRequestDetailSyncState, + upsertPullRequestDetailSyncState, + getRepository, + listUpstreamDriftReports, + listInstallationHealth, + listProductUsageDailyRollups, + listProductUsageEvents, + listPullRequests, + listPullRequestFiles, + listRepoSyncStates, + listSignalSnapshots, + persistSignalSnapshot, + recordGateBlockOutcome, + markGateOutcomeOverridden, + recordProductUsageEvent, + upsertAgentCommandAnswer, + upsertCheckSummary, + upsertIssueFromGitHub, + upsertRepoSyncSegment, + upsertInstallation, + updatePullRequestSlopAssessment, + upsertOfficialMinerDetection, + upsertPullRequestFile, + upsertPullRequestFromGitHub, + upsertIssueWatchSubscription, + upsertRepositoryAiKey, + upsertRepositorySettings, + upsertRepositoryFromGitHub, + putCachedAiReview, + markAiReviewPublished, + putCachedAiSlopAdvisory, + putCachedLinkedIssueSatisfaction, + recordReviewSuppression, + listReviewSuppressions, + setGlobalAgentFrozen, +} from "../../src/db/repositories"; +import { agentMaintenanceHeadMatchesGate, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors"; +import type { PullRequestRecord } from "../../src/types"; +import { aiReviewCacheInputFingerprint } from "../../src/review/ai-review-cache-input"; +import { fingerprint as reviewMemoryFingerprint } from "../../src/review/review-memory-match"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; +import * as focusManifestLoaderModule from "../../src/signals/focus-manifest-loader"; +import { normalizeRegistryPayload } from "../../src/registry/normalize"; +import { persistRegistrySnapshot } from "../../src/registry/sync"; +import { + classifyPullRequestFreshness, + fetchPullRequestFreshness, +} from "../../src/github/pr-freshness"; +import { createTestEnv } from "../helpers/d1"; +import { ISSUE_WAKE_MAX_PRS, MERGE_WAKE_MAX_PRS, SWEEP_MAX_PRS } from "../../src/settings/agent-sweep"; +import { AGENT_LABEL_PENDING_CLOSURE, DEFAULT_LINKED_ISSUE_HARD_RULES } from "../../src/review/linked-issue-hard-rules"; + +vi.mock("../../src/github/pr-freshness", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchPullRequestFreshness: vi.fn(async (_env: Env, args: { expectedHeadSha?: string | null }) => ({ + status: "current" as const, + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [] as string[], + })), + }; +}); + +// The re-gate sweep now FANS OUT the heavy re-review + marker stamp into per-PR `agent-regate-pr` jobs +// (#audit-sweep-fanout). A test asserting the re-review/stamp side effects must run the sweep AND drain the +// per-PR jobs it enqueues. Returns the captured agent-regate-pr jobs for assertions. +async function sweepAndDrainPerPr(env: Env, repoFullName: string): Promise { + const fanned: import("../../src/types").JobMessage[] = []; + const send = env.JOBS.send.bind(env.JOBS); + env.JOBS.send = (async (message: import("../../src/types").JobMessage, options?: QueueSendOptions) => { + if (message.type === "agent-regate-pr") fanned.push(message); + return send(message, options); + }) as typeof env.JOBS.send; + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName }); + env.JOBS.send = send; + for (const job of fanned) await processJob(env, job); + return fanned; +} + + +function completeSegment(repoFullName: string, segment: "labels" | "open_issues" | "open_pull_requests") { + return { + repoFullName, + segment, + status: "complete" as const, + sourceKind: "test" as const, + mode: "resume" as const, + fetchedCount: 1, + expectedCount: 1, + pageCount: 1, + completedAt: "2026-05-25T00:00:00.000Z", + warnings: [], + }; +} + +type CommandAnswerFixture = Parameters[1]; + +function commandAnswer(id: string, command: string, overrides: Partial = {}): CommandAnswerFixture { + return { + id, + repoFullName: "JSONbored/gittensory", + issueNumber: 77, + command, + requestCommentId: 7, + responseCommentId: 9001, + responseUrl: "https://github.com/JSONbored/gittensory/pull/77#issuecomment-9001", + actorKind: "maintainer" as const, + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + metadata: {}, + ...overrides, + }; +} + +function commandAnswerBody(answerId: string, command: string): string { + return [ + "", + ``, + `Command: \`@gittensory ${command}\``, + "Feedback is aggregate-only.", + ].join("\n"); +} + +function queueMinerSnapshot(login: string) { + return { + source: "gittensor_api" as const, + githubId: "123", + githubUsername: login, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + issueDiscoveryScore: 0, + issueTokenScore: 0, + issueCredibility: 1, + isIssueEligible: false, + issueEligibleRepoCount: 0, + alphaPerDay: 0, + taoPerDay: 0, + usdPerDay: 0, + totals: { + pullRequests: 3, + mergedPullRequests: 2, + openPullRequests: 1, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + }, + repositories: [], + pullRequests: [], + issueLabels: [], + }; +} + +function b64(value: string): string { + return Buffer.from(value, "utf8").toString("base64"); +} + +function withProductUsageInsertFailure(env: Env): Env { + const db = env.DB as unknown as { prepare(sql: string): unknown; batch(statements: unknown[]): Promise }; + return { + ...env, + DB: { + prepare(sql: string) { + if (sql.includes("product_usage_events")) throw new Error("product usage insert failed"); + return db.prepare.call(db, sql); + }, + batch(statements: unknown[]) { + return db.batch.call(db, statements); + }, + } as unknown as D1Database, + }; +} + +async function generatePrivateKeyPem(): Promise { + const key = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); + const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; +} + +describe("queue processors", () => { + // Freshness-SLO fixtures are dated relative to late May 2026; pin the clock so staleness windows + // stay deterministic regardless of when CI runs. + beforeEach(() => { + clearInstallationTokenCacheForTest(); + clearReviewSuppressionCacheForTest(); + vi.mocked(fetchPullRequestFreshness).mockReset(); + vi.mocked(fetchPullRequestFreshness).mockImplementation(async (_env, args) => ({ + status: "current", + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [], + })); + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-05-28T00:00:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + async function cachedSubFloorDefectFingerprint(title: string): Promise { + return aiReviewCacheInputFingerprint({ + title, + mode: "block", + byok: false, + provider: null, + model: null, + aiReviewAllAuthors: false, + aiReviewCloseConfidence: undefined, + aiReviewCombine: null, + aiReviewOnMerge: null, + aiReviewReviewers: null, + gatePack: "oss-anti-slop", + reviewerPlan: undefined, + selfHostProviderConfig: null, + selfHostAiModelOverride: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null, ollamaModel: null, openaiModel: null, openaiCompatibleModel: null, anthropicModel: null }, + reviewFiles: [{ path: "src/a.ts", status: "modified", patch: "@@\n+export const ok = value.length;", additions: 1, deletions: 0 }], + profile: null, + securityFocus: false, + inlineComments: false, + pathInstructions: [], + pathGuidance: "", + repoInstructions: null, + excludePaths: [], + pathFilters: [], + changedPaths: ["src/a.ts"], + features: { grounding: false, rag: false, enrichment: false, reputation: false, cultureProfile: false, impactMap: false }, + }); + } + + it("#4603: a sub-floor cached ai_consensus_defect under hold_for_review (default) still fails the gate but does NOT one-shot-close", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); + // aiReviewLowConfidenceDisposition left UNSET — the shipped default (hold_for_review) is what's under test. + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { close: "auto" }, aiReviewMode: "block", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 8, title: "Sub-floor defect PR", state: "open", user: { login: "contributor" }, head: { sha: "b8" }, labels: [], body: "Closes #1" }); + await upsertPullRequestFile(env, { repoFullName: "owner/agent-repo", pullNumber: 8, path: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, payload: { patch: "@@\n+export const ok = value.length;" } }); + const inputFingerprint = await cachedSubFloorDefectFingerprint("Sub-floor defect PR"); + await putCachedAiReview(env, "owner/agent-repo", 8, "b8", "block", { + notes: "cached review", + reviewerCount: 2, + // 0.3 is well below the default 0.93 close-confidence floor. + findings: [{ code: "ai_consensus_defect", severity: "critical", title: "Cached defect", detail: "Cached critical defect.", confidence: 0.3 }], + metadata: { inputFingerprint }, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/8/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = value.length;" }]); + if (url.endsWith("/pulls/8") && init?.method === "PATCH") return Response.json({ number: 8, state: "closed" }); + if (url.endsWith("/pulls/8")) return Response.json({ number: 8, title: "Sub-floor defect PR", state: "open", user: { login: "contributor" }, head: { sha: "b8" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/b8/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/b8/status")) return Response.json({ state: "success", statuses: [] }); + if (url.endsWith("/pulls/8/reviews") && init?.method === "POST") return Response.json({ id: 1 }); + if (url.endsWith("/pulls/8/reviews")) return Response.json([]); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await sweepAndDrainPerPr(env, "owner/agent-repo"); + + expect(aiCalls).toBe(0); // the cached AI review was reused — the LLM was never called for this head SHA + // The gate still failed on the AI-judgment blocker (the merge stays blocked). + const blocker = await env.DB.prepare("select blocker_codes_json from gate_outcomes where repo_full_name = ? and pull_number = ? order by rowid desc limit 1").bind("owner/agent-repo", 8).first<{ blocker_codes_json: string }>(); + expect(blocker?.blocker_codes_json).toContain("ai_consensus_defect"); + // But it was NOT one-shot-closed -- the hold suppressed the close autonomy would otherwise have taken. + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and detail like ?").bind("agent.action.close", "%closed%").first<{ n: number }>(); + expect(closeAudit?.n).toBe(0); + const pr8 = await getPullRequest(env, "owner/agent-repo", 8); + expect(pr8?.state).toBe("open"); + }); + + it("#4603: the SAME sub-floor defect one-shot-closes when aiReviewLowConfidenceDisposition is explicitly one_shot", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { pull_requests: "write" }, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { close: "auto" }, aiReviewMode: "block", aiReviewLowConfidenceDisposition: "one_shot", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 9, title: "Sub-floor defect PR (one_shot)", state: "open", user: { login: "contributor" }, head: { sha: "c9" }, labels: [], body: "Closes #1" }); + await upsertPullRequestFile(env, { repoFullName: "owner/agent-repo", pullNumber: 9, path: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, payload: { patch: "@@\n+export const ok = value.length;" } }); + const inputFingerprint = await cachedSubFloorDefectFingerprint("Sub-floor defect PR (one_shot)"); + await putCachedAiReview(env, "owner/agent-repo", 9, "c9", "block", { + notes: "cached review", + reviewerCount: 2, + findings: [{ code: "ai_consensus_defect", severity: "critical", title: "Cached defect", detail: "Cached critical defect.", confidence: 0.3 }], + metadata: { inputFingerprint }, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/9/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = value.length;" }]); + if (url.endsWith("/pulls/9") && init?.method === "PATCH") return Response.json({ number: 9, state: "closed" }); + if (url.endsWith("/pulls/9")) return Response.json({ number: 9, title: "Sub-floor defect PR (one_shot)", state: "open", user: { login: "contributor" }, head: { sha: "c9" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/c9/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/c9/status")) return Response.json({ state: "success", statuses: [] }); + if (url.endsWith("/pulls/9/reviews") && init?.method === "POST") return Response.json({ id: 1 }); + if (url.endsWith("/pulls/9/reviews")) return Response.json([]); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await sweepAndDrainPerPr(env, "owner/agent-repo"); + + expect(aiCalls).toBe(0); + const blocker = await env.DB.prepare("select blocker_codes_json from gate_outcomes where repo_full_name = ? and pull_number = ? order by rowid desc limit 1").bind("owner/agent-repo", 9).first<{ blocker_codes_json: string }>(); + expect(blocker?.blocker_codes_json).toContain("ai_consensus_defect"); + // one_shot ignores the floor: the close autonomy actually fires this time (contrast with the hold_for_review + // test above, whose closeAudit count is 0). The PR row's `state` column only flips once GitHub's own + // `closed` webhook round-trips back through the normal sync path -- a separate delivery this sweep-driven + // test does not simulate (see the identical gap documented at this file's #linked-issue-hard-rule-persistence + // two-pass test), so the disposition planner's own audit record is the observable proof instead. + const close = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1").bind("agent.action.close").first<{ outcome: string; detail: string }>(); + expect(close?.outcome).toBe("completed"); + }); + + it("posts the 🟪 reviewing placeholder before the AI review runs, then overwrites it with the verdict (#reviewing-placeholder)", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => ({ response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }), + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + const stickyComment: { current: { id: number; body: string } | null } = { current: null }; + let firstWriteWasPlaceholder = false; + let postCount = 0; + let patchCount = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/7/comments") && method === "GET") { + return Response.json(stickyComment.current ? [{ ...stickyComment.current, user: { login: "gittensory[bot]", type: "Bot" } }] : []); + } + if (url.includes("/issues/7/comments") && method === "POST") { + const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + postCount += 1; + if (postCount === 1) firstWriteWasPlaceholder = body.includes("is reviewing"); + stickyComment.current = { id: 1, body }; + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes("/issues/comments/1") && method === "PATCH") { + const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + patchCount += 1; + stickyComment.current = { id: 1, body }; + return Response.json({ id: 1 }, { status: 200 }); + } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "reviewing-placeholder", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, + }, + }); + + // The transient purple placeholder is the first write, then the final verdict updates the same sticky comment. + expect(postCount).toBe(1); + expect(patchCount).toBeGreaterThanOrEqual(1); + expect(firstWriteWasPlaceholder).toBe(true); + expect(stickyComment.current?.body).toContain(PR_PANEL_COMMENT_MARKER); + expect(stickyComment.current?.body).toContain("Thanks for the contribution"); + expect(stickyComment.current?.body).not.toContain("is reviewing"); + }); + + it("flags an open-PR file-path collision against a sibling PR when GITTENSORY_OPEN_PR_FILE_COLLISION is on (#2653)", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_OPEN_PR_FILE_COLLISION: "true", + }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + aiReviewMode: "off", + }); + // A sibling PR (different author, unrelated title) already open and already detail-synced — its files are + // in the pull_request_files cache, the same way routine backfill would have populated them. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 8, + title: "Document logging output", + state: "open", + user: { login: "other-author" }, + head: { sha: "b8" }, + labels: [], + body: "", + }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 8, path: "src/shared/util.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + // The PR under review (#7) was ALSO already detail-synced against the same file before this rerun. + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 7, path: "src/shared/util.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + const stickyComment: { current: { id: number; body: string } | null } = { current: null }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([{ uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/shared/util.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Improve widget rendering", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") { + const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + stickyComment.current = { id: 1, body }; + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "open-pr-file-collision", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Improve widget rendering", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }, + }, + }); + + // The sibling PR #8 (different author, same file, unrelated title) surfaces in the related-work panel — + // proof the enriched changedFiles flowed through buildCollisionReport's existing termOverlap scoring. + expect(stickyComment.current?.body).toContain("#8"); + }); + + it("does NOT flag an open-PR file-path collision when GITTENSORY_OPEN_PR_FILE_COLLISION is unset (byte-identical default)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + aiReviewMode: "off", + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 8, + title: "Document logging output", + state: "open", + user: { login: "other-author" }, + head: { sha: "b8" }, + labels: [], + body: "", + }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 8, path: "src/shared/util.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 7, path: "src/shared/util.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + const stickyComment: { current: { id: number; body: string } | null } = { current: null }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([{ uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/shared/util.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Improve widget rendering", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") { + const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + stickyComment.current = { id: 1, body }; + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "open-pr-file-collision-flag-off", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Improve widget rendering", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }, + }, + }); + + expect(stickyComment.current?.body).not.toContain("#8"); + }); + + it("computes the AI review cache fingerprint with a self-host reviewer plan and converged grounding/enrichment on (#2119)", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => { + aiCalls += 1; + return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; + }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + // A self-host reviewer plan (not just BYOK/cloud provider/model) plus its underlying provider config. + AI_REVIEW_PLAN: { reviewers: [{ model: "claude-code" }], combine: "single" } as never, + CLAUDE_AI_MODEL: "sonnet", + CLAUDE_AI_EFFORT: "high", + // Grounding + enrichment ON, with the repo allowlisted for convergence, so both feature flags + // resolve past their `isXEnabled(env) && convergedRepoAllowed` check into the fingerprint. + GITTENSORY_REVIEW_GROUNDING: "true", + GITTENSORY_REVIEW_ENRICHMENT: "true", + REES_URL: "https://rees.example", + GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) + return Response.json([ + { filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }, + // GitHub omits `patch` for binary/oversized files -- the fingerprint must still normalize this case. + { filename: "assets/logo.png", status: "modified", additions: 0, deletions: 0, changes: 0 }, + ]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + // REES enrichment + any other unmatched call degrade fail-open on a generic empty response. + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "self-host-plan-converged-features", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, + }, + }); + + // The review ran fresh (no pre-seeded cache to reuse), reaching the fingerprint computation with the + // self-host reviewer plan, its provider config, and both converged feature checks evaluated. + expect(aiCalls).toBeGreaterThan(0); + }); + + it("computes the AI review cache fingerprint with the repo quality-culture profile on, both the global flag and the per-repo opt-in (#2995)", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => { + aiCalls += 1; + return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; + }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + // Both gates on: the global capability switch, and — unlike grounding/enrichment/RAG/reputation, which are + // env-only — the per-repo `.gittensory.yml` opt-in mocked below, so `dynamicReviewFeatures.cultureProfile` + // (src/queue/processors.ts) actually evaluates its `&&` right-hand side true, not just short-circuits. + GITTENSORY_REVIEW_CULTURE_PROFILE: "true", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + // The repo's own review.culture_profile opt-in. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n culture_profile: true\n"); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "culture-profile-converged-feature", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, + }, + }); + + // The review ran fresh, reaching the fingerprint computation with the culture-profile feature evaluated — + // this repo has no merge history seeded, so the context itself is empty, but the FLAG combination (not the + // context content) is what dynamicReviewFeatures.cultureProfile tracks for cache-bypass purposes. + expect(aiCalls).toBeGreaterThan(0); + }); + + it("marks a cached AI review non-durable (cacheable=0) when the impact-map feature is on, even with grounding/rag/enrichment/reputation all off (#2182-#2186)", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => { + aiCalls += 1; + return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; + }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + // Both gates on: the global capability switch, and (like culture-profile above, unlike + // grounding/enrichment/RAG/reputation which are env-only) the per-repo `.gittensory.yml` opt-in mocked + // below, so `dynamicReviewFeatures.impactMap` (src/queue/processors.ts) actually evaluates + // shouldComputeImpactMap's `&&` right-hand side true, not just short-circuits. + GITTENSORY_REVIEW_IMPACT_MAP: "true", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + // The repo's own review.impact_map opt-in. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n impact_map: true\n"); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "impact-map-non-durable", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, + }, + }); + + expect(aiCalls).toBeGreaterThan(0); + const cached = await env.DB.prepare("select cacheable from ai_review_cache where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 7, "a7") + .first<{ cacheable: number }>(); + // Never durably cacheable on its own merits, even though grounding/rag/enrichment/reputation are all off in + // this env -- impact-map alone is enough to trip dynamicReviewContextActive. + expect(cached?.cacheable).toBe(0); + }); + + it("reuses a dynamic-context (grounding) AI review indefinitely once published, even long past the old cooldown window (#2119, #regate-churn)", async () => { + // Grounding/RAG/enrichment/reputation each pull TIME-VARYING external context (live CI checks, the vector + // index, REES/CVE data, reputation) that can change for the SAME head SHA without the feature flags + // themselves flipping — so treating a hit here as an INDEFINITELY durable result BEFORE it is ever published + // could replay a review built against now-stale context forever. #regate-churn (root-caused in production: a + // single dynamic-context PR generated 259 of 281 AI review calls in 24h at an unchanged head, because this + // used to re-run UNCONDITIONALLY on every single call, with no bound at all) FIRST changed this to a bounded, + // non-durable reuse (AI_REVIEW_NON_CACHEABLE_RETRY_COOLDOWN_MS) — but that bound was itself still an + // UNBOUNDED total spend over the PR's lifetime (one fresh call every cooldown window, forever). Once the + // review has actually been PUBLISHED to the PR, `published_at` makes it authoritative for its exact + // head+fingerprint regardless of how much time elapses — only a real content/config change or an explicit + // maintainer force-rerun may spend another one. + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => { + aiCalls += 1; + return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; + }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + GITTENSORY_REVIEW_GROUNDING: "true", + GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + const webhook = { + type: "github-webhook" as const, + eventName: "pull_request" as const, + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" as const } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, + }, + }; + await processJob(env, { ...webhook, deliveryId: "dynamic-context-bypass-1" }); + const firstRunAiCalls = aiCalls; + expect(firstRunAiCalls).toBeGreaterThan(0); + const cached = await env.DB.prepare("select cacheable, published_at as publishedAt from ai_review_cache where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 7, "a7") + .first<{ cacheable: number; publishedAt: string | null }>(); + expect(cached?.cacheable).toBe(0); // never durably cacheable on its own merits + expect(cached?.publishedAt).not.toBeNull(); // but it WAS published to the PR this pass + + // Re-review of the SAME head with the SAME (unchanged) inputs, shortly after: reused, no additional LLM spend. + vi.setSystemTime(new Date("2026-05-28T00:05:00.000Z")); + await processJob(env, { ...webhook, deliveryId: "dynamic-context-bypass-2" }); + expect(aiCalls).toBe(firstRunAiCalls); + + // What used to be the cooldown window (30 min) elapses, then a full day, then a full month — the published + // snapshot is authoritative regardless: none of these buy a fresh call. + for (const later of ["2026-05-28T00:31:00.000Z", "2026-05-29T00:00:00.000Z", "2026-06-28T00:00:00.000Z"]) { + vi.setSystemTime(new Date(later)); + await processJob(env, { ...webhook, deliveryId: `dynamic-context-bypass-later-${later}` }); + } + expect(aiCalls).toBe(firstRunAiCalls); + }); + + it("continues to final verdict when the reviewing placeholder audit write fails", async () => { + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.reviewing_placeholder_failed") + throw new Error("D1 audit failed"); + await originalRecordAuditEvent(auditEnv, event); + }); + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => ({ response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }), + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + const postedBodies: string[] = []; + let postAttempts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/47/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/47")) return Response.json({ number: 47, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a47" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a47/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a47/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/47/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/47/comments") && method === "POST") { + postAttempts += 1; + const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + if (postAttempts === 1) return new Response(JSON.stringify({ message: "temporary comment failure" }), { status: 500 }); + postedBodies.push(body); + return Response.json({ id: 47 }, { status: 201 }); + } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "reviewing-placeholder-audit-fails", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 47, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a47" }, labels: [], body: "Closes #1" }, + }, + }); + + expect(postAttempts).toBeGreaterThanOrEqual(2); + expect(postedBodies.some((body) => !body.includes("is reviewing"))).toBe(true); + expect(auditSpy).toHaveBeenCalledWith( + env, + expect.objectContaining({ eventType: "github_app.reviewing_placeholder_failed" }), + ); + auditSpy.mockRestore(); + }); + + it("posts the 🟪 reviewing placeholder for non-AI comment refreshes, then overwrites it with the verdict", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "false", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot(env, normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z")); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "advisory" }); + const stickyComment: { current: { id: number; body: string } | null } = { current: null }; + let postCount = 0; + let patchCount = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/8/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/8")) return Response.json({ number: 8, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a8/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a8/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/8/comments") && method === "GET") { + return Response.json(stickyComment.current ? [{ ...stickyComment.current, user: { login: "gittensory[bot]", type: "Bot" } }] : []); + } + if (url.includes("/issues/8/comments") && method === "POST") { + const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + postCount += 1; + stickyComment.current = { id: 1, body }; + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes("/issues/comments/1") && method === "PATCH") { + const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + patchCount += 1; + stickyComment.current = { id: 1, body }; + return Response.json({ id: 1 }, { status: 200 }); + } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "reviewing-placeholder-disabled-ai", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 8, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }, + }, + }); + + expect(aiCalls).toBe(0); + expect(postCount).toBe(1); + expect(patchCount).toBeGreaterThanOrEqual(1); + expect(stickyComment.current?.body).toContain(PR_PANEL_COMMENT_MARKER); + expect(stickyComment.current?.body).toContain("Thanks for the contribution"); + expect(stickyComment.current?.body).not.toContain("is reviewing"); + }); + + it("keeps the PR comment in 🟪 reviewing state and retries when the final comment update is rate-limited", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "false", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + aiReviewMode: "advisory", + }); + const postedBodies: string[] = []; + let finalCommentAttempted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/9/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/9")) return Response.json({ number: 9, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a9/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a9/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/9/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/9/comments") && method === "POST") { + const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + if (postedBodies.length === 0) { + postedBodies.push(body); + return Response.json({ id: 1 }, { status: 201 }); + } + finalCommentAttempted = true; + return new Response(JSON.stringify({ message: "API rate limit exceeded" }), { + status: 403, + headers: { "x-ratelimit-remaining": "0" }, + }); + } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "reviewing-placeholder-comment-ratelimit", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 9, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "Closes #1" }, + }, + }), + ).rejects.toThrow(/rate limit/i); + + expect(finalCommentAttempted).toBe(true); + expect(postedBodies).toHaveLength(1); + expect(postedBodies[0]).toContain("is reviewing"); + expect(postedBodies[0]).toContain("🟪"); + }); + + it("publishes AI notes when the review omits a narrative assessment", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => { + aiCalls += 1; + return { + response: JSON.stringify({ + assessment: "", + blockers: [], + nits: ["Add coverage for the new branch."], + suggestions: ["Add coverage for the new branch."], + }), + }; + }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await putCachedAiReview(env, "JSONbored/gittensory", 10, "a10", "block", { + notes: "**Nits (1)**\n- stale cached nit", + reviewerCount: 1, + }); + const commentBodies: string[] = []; + const checkPatches: Array<{ status?: string; conclusion?: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/10/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/10")) return Response.json({ number: 10, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a10" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a10/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a10/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/10/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/10/comments") && method === "POST") { + commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "")); + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 971 }, { status: 201 }); + if (url.includes("/check-runs/971") && method === "PATCH") { + checkPatches.push(JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }); + return Response.json({ id: 971 }); + } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "reviewing-placeholder-ai-summary-missing", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 10, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a10" }, labels: [], body: "Closes #1" }, + }, + }), + ).resolves.toBeUndefined(); + + expect(commentBodies.length).toBeGreaterThanOrEqual(2); + expect(commentBodies[0]).toContain("is reviewing"); + expect(commentBodies[0]).toContain("🟪"); + const finalComment = commentBodies.find((body) => !body.includes("is reviewing")); + expect(finalComment).toBeDefined(); + expect(finalComment).toContain("Readiness score"); + expect(finalComment).not.toContain("stale cached nit"); + expect(finalComment).toContain("did not include a separate narrative summary"); + expect(finalComment).toContain("Add coverage for the new branch."); + expect(aiCalls).toBeGreaterThan(0); + expect(checkPatches).toContainEqual(expect.objectContaining({ status: "completed" })); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?") + .bind("github_app.ai_review_public_summary_missing") + .first<{ n: number }>(); + expect(audit?.n).toBe(0); + }); + + it("publishes a non-cacheable AI-unavailable note when no reviewer returns usable output", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => ({ response: "not-json" }), + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 48, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a48" }, labels: [], body: "Closes #1" }); + const commentBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/48/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/48")) return Response.json({ number: 48, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a48" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a48/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a48/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/48/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/48/comments") && method === "POST") { + commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "")); + return Response.json({ id: 48 }, { status: 201 }); + } + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await expect( + processJob(env, { + type: "agent-regate-pr", + deliveryId: "regate-ai-unavailable", + repoFullName: "JSONbored/gittensory", + prNumber: 48, + installationId: 123, + }), + ).resolves.toBeUndefined(); + + expect(commentBodies.length).toBeGreaterThanOrEqual(2); + expect(commentBodies[0]).toContain("is reviewing"); + const finalComment = commentBodies.find((body) => !body.includes("is reviewing")); + expect(finalComment).toContain("Gittensory review needs maintainer review"); + expect(finalComment).toContain("AI review could not be completed for this PR head"); + expect(finalComment).not.toContain("The AI reviewer returned public review text but not the expected structured verdict"); + // #regate-churn: the "AI review could not be completed" outcome is now PERSISTED (so a repeated scheduled + // sweep pass at the same head can reuse it for a bounded cooldown instead of re-spending an LLM call every + // tick) but marked non-durable (cacheable=0) — it must never be replayed as a trustworthy, indefinitely-valid + // verdict. + const cached = await env.DB.prepare("select cacheable from ai_review_cache where repo_full_name = ? and pull_number = ?") + .bind("JSONbored/gittensory", 48) + .first<{ cacheable: number }>(); + expect(cached?.cacheable).toBe(0); + const nonCacheableAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?") + .bind("github_app.ai_review_non_cacheable") + .first<{ n: number }>(); + expect(nonCacheableAudit?.n).toBe(1); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?") + .bind("github_app.ai_review_public_summary_missing") + .first<{ n: number }>(); + expect(audit?.n).toBe(0); + }); + + it("INVARIANT (#confirmed-bug): a second overlapping pass for the same PR head defers to the AI review lock, holds the gate NEUTRAL, and never calls the AI a second time", async () => { + // Simulates the confirmed TOCTOU race: a webhook pass and an agent-regate-pr sweep pass both reach + // runAiReviewForAdvisory for the SAME PR at the SAME head SHA before either has written the cache. The + // webhook pass (not modeled directly here — job-coalesce keys never match across trigger shapes) is + // simulated by pre-claiming the lock exactly as runAiReviewForAdvisory itself would; the agent-regate-pr + // pass under test must then defer instead of firing its own, potentially-divergent LLM call. + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async () => { + aiCalls += 1; + return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; + }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 49, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a49" }, labels: [], body: "Closes #1" }); + const commentBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/49/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/49")) return Response.json({ number: 49, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a49" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a49/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a49/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/49/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/49/comments") && method === "POST") { + commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "")); + return Response.json({ id: 49 }, { status: 201 }); + } + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + // The "first pass" (webhook-shaped) claims the lock for this exact (repo, PR, head, mode) tuple and is still + // in-flight when the "second pass" (agent-regate-pr sweep-shaped) below reaches runAiReviewForAdvisory. + expect((await claimAiReviewLock(env, "JSONbored/gittensory", 49, "a49", "block")).acquired).toBe(true); + + await expect( + processJob(env, { + type: "agent-regate-pr", + deliveryId: "race-ai-review", + repoFullName: "JSONbored/gittensory", + prNumber: 49, + installationId: 123, + }), + ).resolves.toBeUndefined(); + + // The losing pass never called the AI a second time — it deferred to the lock instead of double-spending. + expect(aiCalls).toBe(0); + const finalComment = commentBodies.find((body) => !body.includes("is reviewing")); + expect(finalComment).toContain("Gittensory review needs maintainer review"); + expect(finalComment).toContain("AI review is already running for this PR head in another Gittensory pass"); + // A lock-contention placeholder must never be persisted at all (not even non-durably, #regate-churn) — the + // concurrent pass it deferred to writes the REAL result within seconds, and replaying this placeholder for + // the rest of a bounded-cooldown window would mask that real result long after the race resolved. + const cached = await env.DB.prepare("select count(*) as n from ai_review_cache where repo_full_name = ? and pull_number = ?") + .bind("JSONbored/gittensory", 49) + .first<{ n: number }>(); + expect(cached?.n).toBe(0); + }); + + it("publishes deterministic surface and reports missing summary when required AI is over quota", async () => { + const aiRun = vi.fn(async () => ({ response: "{}" })); + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: aiRun } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "0", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 49, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a49" }, labels: [], body: "Closes #1" }); + const commentBodies: string[] = []; + const captureSpy = vi.spyOn(sentryModule, "captureReviewFailure"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/49/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/49")) return Response.json({ number: 49, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a49" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a49/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a49/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/49/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/49/comments") && method === "POST") { + commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "")); + return Response.json({ id: 49 }, { status: 201 }); + } + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await expect( + processJob(env, { + type: "agent-regate-pr", + deliveryId: "regate-ai-over-quota", + repoFullName: "JSONbored/gittensory", + prNumber: 49, + installationId: 123, + }), + ).resolves.toBeUndefined(); + + expect(aiRun).not.toHaveBeenCalled(); + expect(commentBodies.length).toBeGreaterThanOrEqual(2); + const finalComment = commentBodies.find((body) => !body.includes("is reviewing")); + expect(finalComment).toContain("Readiness score"); + expect(finalComment).not.toContain("AI review returned public review text"); + const audit = await env.DB.prepare("select event_type, metadata_json from audit_events where event_type = ?") + .bind("github_app.ai_review_public_summary_missing") + .first<{ event_type: string; metadata_json: string }>(); + expect(audit).toMatchObject({ event_type: "github_app.ai_review_public_summary_missing" }); + expect(audit?.metadata_json).toContain('"aiReviewMode":"block"'); + expect(captureSpy).toHaveBeenCalledWith( + expect.any(Error), + expect.objectContaining({ + reason: "ai_review_public_summary_missing", + repo: "JSONbored/gittensory", + pr: 49, + reviewer_count: 0, + public_notes: false, + }), + ); + captureSpy.mockRestore(); + }); + + it("agent re-gate sweep re-reviews each stale open PR (installation id) and swallows a failing re-review", async () => { + const env = createTestEnv({}); + await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, linkedIssueGateMode: "block" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Unlinked PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "no linked issue here" }); + // Advance past the one-hour freshness window so the just-seeded PR reads as stale. + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + // Make the re-review itself REJECT (its advisory persist throws) so the sweep's per-PR error backstop runs. + // Only the advisories insert is poisoned; every other read/write (verdict computation, the closing audit + // event) keeps working — the sweep must still complete and record its advisory verdict. + const realPrepare = env.DB.prepare.bind(env.DB); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + env.DB.prepare = ((sql: string) => { + if (/insert\s+into\s+["'`]?advisories/i.test(sql)) throw new Error("advisory persist failed"); + return realPrepare(sql); + }) as typeof env.DB.prepare; + + await sweepAndDrainPerPr(env, "owner/agent-repo"); + + const audit = await realPrepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ + outcome: string; + metadata_json: string; + }>(); + expect(audit?.outcome).toBe("completed"); + expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ repoFullName: "owner/agent-repo", examined: 1, flagged: 1 }); + // The failing re-review was caught and logged via the sweep_rereview_failed backstop, not rethrown. + expect(errors.mock.calls.some((call) => String(call[0]).includes("sweep_rereview_failed"))).toBe(true); + errors.mockRestore(); + }); + + it("agent re-gate sweep stamps last_regated_at on each recomputed PR so the next sweep advances (#audit-sweep-converge)", async () => { + const env = createTestEnv({}); + await upsertInstallation(env, { action: "created", installation: { id: 9002, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9002); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); + const before = await env.DB.prepare("select last_regated_at from pull_requests where repo_full_name = ? and number = 7").bind("owner/agent-repo").first<{ last_regated_at: string | null }>(); + expect(before?.last_regated_at).toBeNull(); // never swept yet + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + // #2852: autonomy configured (merge: auto) now means the gate CONCLUSION is evaluated even without a + // GITHUB_APP_PRIVATE_KEY / check-run publish, which reaches the review-thread-blockers live fetch -- stub a + // generic safe response so that call resolves instead of hitting a real, unmocked network request. + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url === "https://api.github.com/graphql") return Response.json({ data: {} }); + return Response.json({}); + }); + + await sweepAndDrainPerPr(env, "owner/agent-repo"); + + const after = await env.DB.prepare("select last_regated_at from pull_requests where repo_full_name = ? and number = 7").bind("owner/agent-repo").first<{ last_regated_at: string | null }>(); + expect(typeof after?.last_regated_at).toBe("string"); // stamped via a D1 write at dispatch — convergence does not need a GitHub write + }); + + it("agent re-gate sweep processes strict staleness order even when a PR is missing its current Gate check (#selfhost-fifo-ordering)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9400, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9400); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + for (const number of [1, 2, 3, 4]) { + const headSha = `a${number}`; + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `PR${number}`, state: "open", user: { login: "c" }, head: { sha: headSha }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", number, headSha); + if (number !== 2) { + await upsertCheckSummary(env, { + id: `gate-${number}`, + repoFullName: "owner/agent-repo", + pullNumber: number, + headSha, + name: "Gittensory Orb Review Agent", + status: "completed", + conclusion: "success", + payload: {}, + }); + } + } + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 5, title: "Draft without a head", state: "open", draft: true, user: { login: "c" }, labels: [], body: "" } as never); + // Only PR2 gets a regate stamp (post-#never-endless-reregate, an ordinary already-regated PR is permanently + // excluded from the sweep -- see agent-sweep.test.ts -- so PR1/3/4 must stay never-regated to remain eligible + // ordinary candidates at all). PR2 is missing its current Gate check (surfaceRepairPriorityPullNumbers would + // flag it as a repair candidate), so its repair-priority bypass keeps it eligible DESPITE already having a + // stamp -- this is exactly the scenario the repair-priority bypass exists for. + await env.DB.prepare( + `update pull_requests set last_regated_at = '2026-05-28T01:50:00.000Z' where repo_full_name = ? and number = 2`, + ) + .bind("owner/agent-repo") + .run(); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const fanned = sent.filter((job) => job.type === "agent-regate-pr"); + // PR2 is missing its current Gate check and already has a regate stamp from 10 min ago (very fresh by + // lastRegatedAt), while PR1/3/4 have never been regated at all (the ordinary, post-#never-endless-reregate + // candidate shape). An earlier revision sorted repair candidates first regardless of staleness, jumping PR2 + // to the front of this batch -- that let a PR needing repair cut ahead of older PRs that merely went stale, + // observed live as PRs dispatching out of order ("spraying") whenever a repo had a mixed repair/ordinary + // backlog. Repair status only affects ELIGIBILITY (staying in the pool despite already having a stamp), + // never final order, so PR2 takes its rightful (last, since it's the freshest-regated) place and is dropped + // by the max:3 cap this round -- same as it would be with no repair flag at all. + expect(fanned.map((job) => (job as Extract).prNumber)).toEqual([1, 3, 4]); + }); + + it("REGRESSION (#3815): regateSweepOrderMode 'oldest-first' fans out per-PR jobs in creation order with a monotonic delaySeconds stagger", async () => { + const dispatched: { prNumber: number; delaySeconds: number | undefined }[] = []; + const env = createTestEnv({ + JOBS: { + async send(m: import("../../src/types").JobMessage, options?: { delaySeconds?: number }) { + if (m.type === "agent-regate-pr") dispatched.push({ prNumber: m.prNumber, delaySeconds: options?.delaySeconds ?? 0 }); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9403, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9403); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, regateSweepOrderMode: "oldest-first", gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + // Deliberately seeded out of PR-number order: #1 is the NEWEST, #3 is the OLDEST — proves the fan-out + // follows createdAt, not insertion/number order. + const created: Record = { 1: "2026-05-20T00:00:00.000Z", 2: "2026-05-10T00:00:00.000Z", 3: "2026-05-01T00:00:00.000Z" }; + for (const number of [1, 2, 3]) { + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { + number, + title: `PR${number}`, + state: "open", + user: { login: "c" }, + head: { sha: `a${number}` }, + labels: [], + body: "", + created_at: created[number]!, + updated_at: created[number]!, + }); + } + vi.setSystemTime(new Date("2026-05-28T00:00:00.000Z")); // well past the 2-min webhook-freshness window for all three + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + expect(dispatched.map((d) => d.prNumber)).toEqual([3, 2, 1]); // oldest-created (#3) first, newest (#1) last + expect(dispatched.map((d) => d.delaySeconds)).toEqual([0, 10, 20]); // strictly increasing with dispatch order + }); + + it("REGRESSION: scheduled sweeps repair every missing current Gate check without waiting behind another repo backlog", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + snapshot() { + return { + totals: { pending: 0, processing: 1, dead: 0, due: 0 }, + byType: [ + { + type: "agent-regate-pr", + status: "processing", + count: 1, + due: 0, + }, + ], + }; + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9402, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9402); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + for (const number of [1, 2, 3, 4, 5]) { + const headSha = `repair-${number}`; + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `Repair ${number}`, state: "open", user: { login: "c" }, head: { sha: headSha }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", number, headSha); + } + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); + expect(fanned.map((job) => job.prNumber)).toEqual([1, 2, 3, 4, 5]); + const audit = await env.DB.prepare("select metadata_json from audit_events where event_type = ? and outcome = ?") + .bind("agent.sweep.regate", "completed") + .first<{ metadata_json: string }>(); + expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ + repoFullName: "owner/agent-repo", + examined: 5, + }); + }); + + it("REGRESSION: an active per-PR regate backlog restricts the sweep to priority repairs, not a full stale-PR batch too", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + // A nonzero per-PR regate backlog (agent-regate-pr pending/processing > 0) -- the same signal the + // "waiting behind another repo backlog" deferral above reacts to. + snapshot() { + return { + totals: { pending: 1, processing: 0, dead: 0, due: 1 }, + byType: [{ type: "agent-regate-pr", status: "pending", count: 1, due: 1 }], + }; + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9403, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9403); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + // PR 1: missing its current Gate check -- the one priority repair. Make it newer-by-regate than the + // ordinary stale PRs below, reproducing the backlog bug where a max=1 staleness slice could drop the repair. + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 1, title: "Repair 1", state: "open", user: { login: "c" }, head: { sha: "repair-1" }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 1, "repair-1"); + await env.DB.prepare("update pull_requests set last_regated_at = ? where repo_full_name = ? and number = ?") + .bind("2026-05-28T01:59:00.000Z", "owner/agent-repo", 1) + .run(); + // PRs 2-5: ordinary, already-current, stale-by-time PRs -- a normal (no-backlog) sweep would pick these up + // too, but while the backlog is draining they must sit out so the sweep only carries the priority repair. + for (const number of [2, 3, 4, 5]) { + const headSha = `stale-${number}`; + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `Stale ${number}`, state: "open", user: { login: "c" }, head: { sha: headSha }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", number, headSha); + await env.DB.prepare("update pull_requests set last_regated_at = ? where repo_full_name = ? and number = ?") + .bind(`2026-05-28T01:0${number}:00.000Z`, "owner/agent-repo", number) + .run(); + await upsertCheckSummary(env, { + id: `gate-current-${number}`, + repoFullName: "owner/agent-repo", + pullNumber: number, + headSha, + name: "Gittensory Orb Review Agent", + status: "completed", + conclusion: "success", + payload: {}, + }); + } + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); + expect(fanned.map((job) => job.prNumber)).toEqual([1]); // only the priority repair, not PRs 2-5 + }); + + it("REGRESSION: the sweep tags a priority-repair fan-out with 'regate-repair:' and an ordinary candidate with 'regate-sweep:' (#selfhost-queue-liveness)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9404, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9404); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + // PR 1: missing its current Gate check for its current head -- surfaceRepairPriorityPullNumbers flags this as + // outage-repair priority (no completed Gittensory Gate check run at the live head SHA). + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 1, title: "Repair 1", state: "open", user: { login: "c" }, head: { sha: "repair-1" }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 1, "repair-1"); + // PR 2: ordinary PR with a completed current-head Gate check -- NOT priority. + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 2, title: "Ordinary 2", state: "open", user: { login: "c" }, head: { sha: "ordinary-2" }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 2, "ordinary-2"); + await upsertCheckSummary(env, { + id: "gate-current-2", + repoFullName: "owner/agent-repo", + pullNumber: 2, + headSha: "ordinary-2", + name: "Gittensory Orb Review Agent", + status: "completed", + conclusion: "success", + payload: {}, + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); + expect(fanned).toHaveLength(2); + const repairJob = fanned.find((job) => job.prNumber === 1); + const ordinaryJob = fanned.find((job) => job.prNumber === 2); + expect(repairJob).toMatchObject({ + type: "agent-regate-pr", + deliveryId: "regate-repair:owner/agent-repo#1", + repoFullName: "owner/agent-repo", + prNumber: 1, + installationId: 9404, + }); + expect(ordinaryJob).toMatchObject({ + type: "agent-regate-pr", + deliveryId: "regate-sweep:owner/agent-repo#2", + repoFullName: "owner/agent-repo", + prNumber: 2, + installationId: 9404, + }); + }); + + it("REGRESSION (#orb-retry-storm): after MAX_ATTEMPTS repair dispatches for the SAME head SHA, the sweep stops bypassing freshness and records exactly one repair_exhausted audit event", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9407, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9407); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + // PR 1: missing its current Gate check for its current head -- would ordinarily be flagged outage-repair + // priority on every tick. Pre-seed REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA=5 (#3998) prior repair-attempt audit + // events for this EXACT head SHA to simulate a review that keeps failing (e.g. a timeout) and never + // publishes a completed gate check. + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 1, title: "Stuck repair", state: "open", user: { login: "c" }, head: { sha: "stuck-sha" }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 1, "stuck-sha"); + const targetKey = "owner/agent-repo#1#stuck-sha"; + for (let i = 0; i < 5; i += 1) { + await repositoriesModule.recordAuditEvent(env, { + eventType: "agent.sweep.regate.repair_attempt", + actor: "gittensory", + targetKey, + outcome: "queued", + detail: "prior attempt", + metadata: {}, + }); + } + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + + try { + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + // No longer treated as priority repair -- either not fanned at all, or fanned as an ordinary "regate-sweep:" + // candidate, but never re-dispatched as "regate-repair:" once the same SHA has exhausted its attempt budget. + const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); + expect(fanned.every((job) => job.deliveryId !== "regate-repair:owner/agent-repo#1")).toBe(true); + const exhausted = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") + .bind("agent.sweep.regate.repair_exhausted", targetKey) + .first<{ n: number }>(); + expect(exhausted?.n).toBe(1); + // No further repair-attempt event was recorded for the exhausted SHA this tick. + const attempts = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") + .bind("agent.sweep.regate.repair_attempt", targetKey) + .first<{ n: number }>(); + expect(attempts?.n).toBe(5); + // Sentry-visible signal (via the structured-log forwarder) fires exactly once alongside the audit event. + const exhaustedLogs = errors.mock.calls.filter(([line]) => typeof line === "string" && line.includes("regate_repair_exhausted")); + expect(exhaustedLogs).toHaveLength(1); + const logged = JSON.parse(exhaustedLogs[0]![0] as string) as Record; + expect(logged).toMatchObject({ level: "error", event: "regate_repair_exhausted", repo: "owner/agent-repo", pullNumber: 1, headSha: "stuck-sha", attempts: 5 }); + } finally { + errors.mockRestore(); + } + }, 60_000); + + it("REGRESSION (#orb-retry-storm): a repair dispatch under the attempt cap records a repair_attempt audit event, and a second sweep tick does not duplicate the repair_exhausted event once already flagged", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9408, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9408); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 2, title: "Fresh repair", state: "open", user: { login: "c" }, head: { sha: "fresh-sha" }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 2, "fresh-sha"); + const targetKey = "owner/agent-repo#2#fresh-sha"; + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); + expect(fanned.map((job) => job.deliveryId)).toContain("regate-repair:owner/agent-repo#2"); + // #orb-retry-storm (#3998): repair_attempt is now recorded at EXECUTION time (inside regatePullRequest, + // after rate-limit admission), not at dispatch time -- a deferred/dropped fan-out no longer counts against + // the cap. The sweep only dispatches the per-PR job above; it must actually run for the attempt to land. + await processJob(env, fanned.find((job) => job.deliveryId === "regate-repair:owner/agent-repo#2")!); + const attemptsAfterFirst = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") + .bind("agent.sweep.regate.repair_attempt", targetKey) + .first<{ n: number }>(); + expect(attemptsAfterFirst?.n).toBe(1); + + // Manually push this SHA over the REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA=5 cap (#3998), then run the sweep twice + // more -- the exhausted event must be recorded only once even though the PR is (re-)evaluated on every tick. + for (let i = 0; i < 4; i += 1) { + await repositoriesModule.recordAuditEvent(env, { + eventType: "agent.sweep.regate.repair_attempt", + actor: "gittensory", + targetKey, + outcome: "queued", + detail: "prior attempt", + metadata: {}, + }); + } + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const exhausted = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") + .bind("agent.sweep.regate.repair_exhausted", targetKey) + .first<{ n: number }>(); + expect(exhausted?.n).toBe(1); + }, 60_000); + + it("agent re-gate sweep fail-opens when current Gate check reads fail during repair priority selection", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9401, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9401); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Repair me", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + const realPrepare = env.DB.prepare.bind(env.DB); + env.DB.prepare = ((sql: string) => { + if (/from\s+["`]?check_summaries["`]?/i.test(sql)) throw new Error("check summary read failed"); + return realPrepare(sql); + }) as typeof env.DB.prepare; + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const fanned = sent.filter((job) => job.type === "agent-regate-pr") as Extract[]; + expect(fanned.map((job) => job.prNumber)).toEqual([7]); + }); + + it("scheduled sweeps skip open-PR refresh when an allowlisted repo has not been registered locally yet", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + GITTENSORY_REVIEW_REPOS: "owner/missing-repo", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + const segmentSpy = vi.spyOn(repositoriesModule, "getRepoSyncSegment"); + const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment"); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/missing-repo" }); + + expect(segmentSpy).not.toHaveBeenCalled(); + expect(backfillSpy).not.toHaveBeenCalled(); + expect(sent).toEqual([]); + segmentSpy.mockRestore(); + backfillSpy.mockRestore(); + }); + + it("scheduled sweeps can refresh stale open-PR rows with an Orb enrollment credential", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + ORB_ENROLLMENT_SECRET: "orb-secret", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9406, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9406); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertRepoSyncSegment(env, completeSegment("owner/agent-repo", "open_pull_requests")); + const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockResolvedValueOnce({ + ok: true, + repoFullName: "owner/agent-repo", + segment: "open_pull_requests", + status: "complete", + fetchedCount: 0, + warnings: [], + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests", force: true })); + expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); + backfillSpy.mockRestore(); + }); + + it("REGRESSION: scheduled sweeps refresh stale open-PR rows so missed webhooks cannot hide PRs from repair", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9402, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9402); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertRepoSyncSegment(env, completeSegment("owner/agent-repo", "open_pull_requests")); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.github.com/graphql") { + return Response.json({ + data: { + rateLimit: { remaining: 4999, resetAt: "2026-05-28T03:00:00.000Z" }, + repository: { + issues: { totalCount: 0 }, + openPullRequests: { totalCount: 1 }, + mergedPullRequests: { totalCount: 0 }, + closedPullRequests: { totalCount: 0 }, + labels: { totalCount: 0 }, + }, + }, + }); + } + if (url.includes("/pulls?state=open")) { + return Response.json([ + { + number: 11, + title: "Webhook missed this PR", + state: "open", + user: { login: "contributor" }, + head: { sha: "h11" }, + labels: [], + body: "Fixes #1", + created_at: "2026-05-27T00:00:00.000Z", + updated_at: "2026-05-27T00:00:00.000Z", + }, + ]); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect((await getPullRequest(env, "owner/agent-repo", 11))?.headSha).toBe("h11"); + const fanned = sent.filter((job) => job.type === "agent-regate-pr") as Extract[]; + expect(fanned.map((job) => job.prNumber)).toEqual([11]); + expect(sent.some((job) => job.type === "backfill-pr-details" && job.repoFullName === "owner/agent-repo")).toBe(true); + }); + + it("scheduled sweeps do not duplicate an active open-PR refresh", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9403, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9403); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertRepoSyncSegment(env, { + ...completeSegment("owner/agent-repo", "open_pull_requests"), + status: "running", + }); + const fetchSpy = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Response.json([])); + vi.stubGlobal("fetch", fetchSpy); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect( + fetchSpy.mock.calls + .map((call) => String((call as [RequestInfo | URL, RequestInit?])[0])) + .filter((url) => url === "https://api.github.com/graphql" || url.includes("/pulls?state=open")), + ).toEqual([]); + expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); + }); + + it("scheduled sweeps fail open when open-PR sync state reads and refreshes fail", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9404, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9404); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + const segmentSpy = vi.spyOn(repositoriesModule, "getRepoSyncSegment").mockRejectedValueOnce(new Error("segment read failed")); + const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockRejectedValueOnce(new Error("open PR refresh failed")); + const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests", mode: "light", force: true })); + expect(warn.mock.calls.some((call) => String(call[0]).includes("sweep_open_pr_sync_failed"))).toBe(true); + expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); + segmentSpy.mockRestore(); + backfillSpy.mockRestore(); + warn.mockRestore(); + }); + + it("REGRESSION (#sweep-uninstalled-budget-waste): a scheduled sweep never refreshes open PRs (via the shared GITHUB_PUBLIC_TOKEN) for a registered-but-uninstalled repo, since no per-PR fan-out will ever follow", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + // Registered (e.g. via the subnet registry sync) but NOT installed — no installationId. + await upsertRepositoryFromGitHub(env, { name: "no-install", full_name: "owner/no-install", private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/no-install", autonomy: { merge: "auto" } }); + const segmentSpy = vi.spyOn(repositoriesModule, "getRepoSyncSegment"); + const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment"); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/no-install" }); + + expect(segmentSpy).not.toHaveBeenCalled(); + expect(backfillSpy).not.toHaveBeenCalled(); + expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); + segmentSpy.mockRestore(); + backfillSpy.mockRestore(); + }); + + it("scheduled sweeps DO still refresh open PRs for an installed repo even when GITHUB_PUBLIC_TOKEN is also configured (installation presence gates the skip, not credential kind)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9405, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9405); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockResolvedValueOnce(undefined as never); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests", mode: "light", force: true })); + backfillSpy.mockRestore(); + }); + + it("scheduled sweeps refresh incomplete open-PR sync segments", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9405, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9405); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertRepoSyncSegment(env, { + ...completeSegment("owner/agent-repo", "open_pull_requests"), + status: "partial", + }); + const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockResolvedValueOnce({ + ok: true, + repoFullName: "owner/agent-repo", + segment: "open_pull_requests", + status: "complete", + fetchedCount: 0, + warnings: [], + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests" })); + expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); + backfillSpy.mockRestore(); + }); + + it("scheduled sweeps refresh completed open-PR sync rows whose completion time is missing", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9407, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9407); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + const segmentSpy = vi.spyOn(repositoriesModule, "getRepoSyncSegment").mockResolvedValueOnce({ + ...completeSegment("owner/agent-repo", "open_pull_requests"), + completedAt: undefined, + } as never); + const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockResolvedValueOnce({ + ok: true, + repoFullName: "owner/agent-repo", + segment: "open_pull_requests", + status: "complete", + fetchedCount: 0, + warnings: [], + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests", force: true })); + expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); + segmentSpy.mockRestore(); + backfillSpy.mockRestore(); + }); + + it("REGRESSION (#audit-sweep-dispatch-stamp): ONE sweep stamps ALL candidates AT DISPATCH, so the next fan-out skips the repo as draining — no overlapping sweeps", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9300, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9300); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + for (const number of [7, 8, 9]) { + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `PR${number}`, state: "open", user: { login: "c" }, head: { sha: `a${number}` }, labels: [], body: "" }); + } + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + // Run ONE sweep — but do NOT drain the per-PR jobs (simulate the staggered/deferred re-reviews not having run yet). + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + // The marker is stamped for EVERY candidate immediately at dispatch — NOT waiting on the per-PR jobs. + const stamped = await env.DB.prepare("select count(*) as n from pull_requests where repo_full_name = ? and last_regated_at is not null").bind("owner/agent-repo").first<{ n: number }>(); + expect(stamped?.n).toBe(3); + + // So the very next cron fan-out sees the fresh stamp and SKIPS this repo as draining — the overlap that caused the runaway is gone. + sent.length = 0; + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule" }); + expect(sent.some((m) => m.type === "agent-regate-sweep" && m.repoFullName === "owner/agent-repo")).toBe(false); + const fanout = await env.DB.prepare("select metadata_json from audit_events where event_type = ? order by created_at desc limit 1").bind("agent.sweep.fanout").first<{ metadata_json: string }>(); + expect(JSON.parse(fanout?.metadata_json ?? "{}").skippedDraining).toBeGreaterThanOrEqual(1); + }); + + it("agent re-gate sweep swallows a failing last_regated_at stamp and still completes (#audit-sweep-converge)", async () => { + const env = createTestEnv({}); + await upsertInstallation(env, { action: "created", installation: { id: 9003, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9003); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + // #2852: autonomy configured (merge: auto) now means the gate CONCLUSION is evaluated even without a + // GITHUB_APP_PRIVATE_KEY / check-run publish, which reaches the review-thread-blockers live fetch -- stub a + // generic safe response so that call resolves instead of hitting a real, unmocked network request. + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url === "https://api.github.com/graphql") return Response.json({ data: {} }); + return Response.json({}); + }); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + const stamp = vi.spyOn(repositoriesModule, "markPullRequestsRegated").mockRejectedValueOnce(new Error("D1 write error")); + + await sweepAndDrainPerPr(env, "owner/agent-repo"); + + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); // the sweep still records its verdict; the dispatch-time stamp failure is swallowed + expect(errors.mock.calls.some((call) => String(call[0]).includes("sweep_mark_regated_failed"))).toBe(true); + stamp.mockRestore(); + errors.mockRestore(); + }); + + it("agent re-gate sweep respects the #776 kill-switch: a paused repo records a skip and recomputes nothing (#777)", async () => { + const env = createTestEnv({}); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, agentPaused: true }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale PR", state: "open", user: { login: "contributor" }, head: { sha: "abc" }, labels: [], body: "x" }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const audit = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ + outcome: string; + detail: string; + metadata_json: string; + }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toMatch(/paused/i); + expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ mode: "paused" }); + }); + + it("agent re-gate sweep no-ops safely on a missing repo arg or an un-configured repo (#777)", async () => { + const env = createTestEnv({}); + // (a) a test-mode per-repo job with no repoFullName → defensive early return + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test" }); + // (b) a repo that never opted the agent in → defensive return after settings resolve + await upsertRepositoryFromGitHub(env, { name: "plain-repo", full_name: "owner/plain-repo", private: false, owner: { login: "owner" } }); + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/plain-repo" }); + + const count = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ n: number }>(); + expect(count?.n).toBe(0); + }); + + it("agent re-gate sweep stays quiet when no open PR is stale enough to re-gate (#777)", async () => { + const env = createTestEnv({}); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + // Seeded "now" → within the freshness window → not a candidate; no clock advance. + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Fresh PR", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "x" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); + await upsertCheckSummary(env, { + id: "gate-fresh-7", + repoFullName: "owner/agent-repo", + pullNumber: 7, + headSha: "a7", + name: "Gittensory Orb Review Agent", + status: "completed", + conclusion: "success", + payload: {}, + }); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const count = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ n: number }>(); + expect(count?.n).toBe(0); + }); + + it("INVARIANT: the sweep fans out one agent-regate-pr job per candidate onto the JOBS lane, not inline (#audit-sweep-fanout)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9100, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9100); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 8, title: "PR8", state: "open", user: { login: "c" }, head: { sha: "a8" }, labels: [], body: "" }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const perPr = sent.filter((m): m is Extract => m.type === "agent-regate-pr"); + expect(perPr.map((m) => m.prNumber).sort()).toEqual([7, 8]); // one per candidate + expect(perPr.every((m) => m.installationId === 9100 && m.repoFullName === "owner/agent-repo")).toBe(true); + expect(sent.every((m) => m.type === "agent-regate-pr")).toBe(true); // the heavy work is enqueued, never done inline + }); + + it("INVARIANT (in-flight guard): the fan-out SKIPS a repo whose prior sweep is still draining, enqueues an idle one (#audit-sweep-fanout)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ GITTENSORY_REVIEW_REPOS: "", JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9101, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + for (const name of ["draining", "idle"]) { + await upsertRepositoryFromGitHub(env, { name, full_name: `owner/${name}`, private: false, owner: { login: "owner" } }, 9101); + await upsertRepositorySettings(env, { repoFullName: `owner/${name}`, autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, `owner/${name}`, { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "h1" }, labels: [], body: "" }); + } + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + // owner/draining was just regated (a sweep is mid-drain); owner/idle has never been swept. + await repositoriesModule.markPullRequestRegated(env, "owner/draining", 1); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule" }); // no repoFullName → fan-out path + + const sweepRepos = sent.filter((m): m is Extract => m.type === "agent-regate-sweep").map((m) => m.repoFullName); + expect(sweepRepos).toEqual(["owner/idle"]); // the draining repo is skipped, the idle one enqueued + const fanout = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("agent.sweep.fanout").first<{ metadata_json: string }>(); + expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 1, skippedDraining: 1 }); + }); + + it("INVARIANT (#audit-fanout-dedup): a BURST of fan-outs collapses to ONE — the second claims nothing and audits denied", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9400, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9400); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule" }); // first fan-out claims the window + expect(sent.some((m) => m.type === "agent-regate-sweep" && m.repoFullName === "owner/agent-repo")).toBe(true); + + sent.length = 0; + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule" }); // burst sibling in the same window → deduped + expect(sent.filter((m) => m.type === "agent-regate-sweep")).toEqual([]); // enqueues no redundant sweep + const denied = await env.DB.prepare("select count(*) as n from audit_events where event_type='agent.sweep.fanout' and outcome='denied'").first<{ n: number }>(); + expect(denied?.n).toBe(1); + }); + + it("claimAiReviewLock claims when free, denies when held (per-PR+head+mode, not globally), and release frees it again (#confirmed-bug)", async () => { + const env = createTestEnv({}); + // First claim for this exact (repo, PR, head, mode) succeeds — no prior pass in-flight. + const first = await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"); + expect(first.acquired).toBe(true); + // A second, concurrent pass for the SAME PR at the SAME head and mode (regardless of what triggered it — + // webhook or sweep) is denied while the first is still in-flight — exactly the race this lock exists for. + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(false); + // A DIFFERENT head SHA for the same PR is unaffected — a new commit is a genuinely new review, not a dup. + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha2", "block")).acquired).toBe(true); + // A DIFFERENT mode for the same PR+head is also unaffected — advisory vs block are independent lock keys. + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "advisory")).acquired).toBe(true); + // A DIFFERENT PR in the same repo is unaffected — the lock is per-PR+head+mode, not repo-wide. + expect((await claimAiReviewLock(env, "owner/agent-repo", 8, "sha1", "block")).acquired).toBe(true); + // Release (the finally block's job) frees the (PR, head, mode) tuple — a subsequent pass can claim it again. + await releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", first.ownerToken); + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); + }); + + it("claimAiReviewLock fails OPEN on a broken transient cache — never itself blocks a real review from running (#confirmed-bug)", async () => { + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async () => { throw new Error("cache read error"); }, + set: async () => { throw new Error("cache write error"); }, + del: async () => { throw new Error("cache delete error"); }, + }, + }); + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); + await expect(releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", null)).resolves.toBeUndefined(); + }); + + it("claimAiReviewLock fails OPEN when no transient cache is configured at all — nothing to serialize against (#confirmed-bug)", async () => { + const env = createTestEnv({}); + delete env.SELFHOST_TRANSIENT_CACHE; + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); + }); + + it("claimAiReviewLock fails OPEN when the atomic claim primitive itself throws (#confirmed-bug)", async () => { + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async () => null, + set: async () => undefined, + claim: async () => { throw new Error("redis unavailable"); }, + }, + }); + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); + }); + + it("REGRESSION: claimAiReviewLock uses an atomic check-and-set, so two genuinely concurrent claims for the SAME (repo, PR, head, mode) can never both succeed", async () => { + // A get-then-set pair has a window between the read and the write where two concurrent callers can both + // observe an absent key and both claim it — exactly what this lock exists to prevent (a webhook pass and a + // sweep pass both missing the cache and both firing a real LLM call). This test races two claims for the + // same tuple via Promise.all (both kick off before either resolves) against the default test cache's + // claim(), which mirrors createRedisCache's atomic SET NX: the check-and-set happens with no `await` + // boundary in between, so it is impossible for both callers to see "unclaimed". + const env = createTestEnv({}); + const [first, second] = await Promise.all([ + claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"), + claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"), + ]); + expect([first, second].filter((claim) => claim.acquired)).toHaveLength(1); + }); + + it("REGRESSION: claimAiReviewLock calls the atomic claim primitive, not a separate get+set pair, when the cache supports it", async () => { + const calls: string[] = []; + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async () => { calls.push("get"); return null; }, + set: async () => { calls.push("set"); }, + claim: async () => { calls.push("claim"); return true; }, + releaseIfValue: async () => true, + }, + }); + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); + expect(calls).toEqual(["claim"]); // never falls through to the racy get/set pair when claim is available + }); + + it("claimAiReviewLock returns true unconditionally when the cache has no claim() — no false exclusivity guarantee (#confirmed-bug, review round 2)", async () => { + // A prior version of this helper fell back to a get-then-set pair (even with an extra write-then-verify + // re-read) when claim() wasn't available. That is NOT a real exclusivity guarantee: caller A can write its + // own token, read it straight back, and return true entirely before caller B's later write/read also + // completes and also returns true -- both callers "win". Rather than pretend to serialize via a check that + // silently fails under exactly the concurrent load this lock exists to guard against (duplicate LLM calls), + // a cache without claim() now gets NO exclusivity at all -- every call proceeds, sequential or concurrent, + // even for a key a previous call already "set" via get/set. + const values = new Map(); + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async (key: string) => values.get(key) ?? null, + set: async (key: string, value: string) => { values.set(key, value); }, + }, + }); + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); + expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); + }); + + it("REGRESSION (#confirmed-bug, review round 2): claimAiReviewLock does not falsely claim exclusivity for two genuinely concurrent callers when the cache has no claim()", async () => { + // Documents the corrected, honest contract under the exact interleaving the gate flagged: with no atomic + // claim() primitive, BOTH concurrent callers proceed (true) -- a webhook pass and a sweep pass racing for + // the same PR head both fire their LLM call, same as before this lock existed, rather than one of them + // wrongly believing it has exclusive ownership when it doesn't. + const values = new Map(); + const yieldThenRun = (fn: () => T): Promise => new Promise((resolve) => queueMicrotask(() => resolve(fn()))); + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: (key: string) => yieldThenRun(() => values.get(key) ?? null), + set: (key: string, value: string) => yieldThenRun(() => { values.set(key, value); }), + }, + }); + const [first, second] = await Promise.all([ + claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"), + claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"), + ]); + expect([first.acquired, second.acquired]).toEqual([true, true]); + }); + + // claimPrActuationLock (#2129/#2135) is the ONE shared per-PR actuation lock: maybeRunAgentMaintenance, + // maybeCloseDraftDodgeAttempt, and maybeRecloseDisallowedReopen all claim/release the SAME key so none of the + // three mutating PR paths can race any other (review round 4) — a single namespace, not one lock per path. + it("claimPrActuationLock claims when free, denies when held (per-PR), and release frees it again (#2135)", async () => { + const env = createTestEnv({}); + const first = await claimPrActuationLock(env, "owner/act-repo", 7); + expect(first.acquired).toBe(true); + expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(false); + expect((await claimPrActuationLock(env, "owner/act-repo", 8)).acquired).toBe(true); + await releasePrActuationLock(env, "owner/act-repo", 7, first.ownerToken); + expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); + }); + + it("claimPrActuationLock fails OPEN on a broken transient cache — never itself blocks actuation (#2135)", async () => { + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async () => { throw new Error("cache read error"); }, + set: async () => { throw new Error("cache write error"); }, + del: async () => { throw new Error("cache delete error"); }, + }, + }); + expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); + await expect(releasePrActuationLock(env, "owner/act-repo", 7, null)).resolves.toBeUndefined(); + }); + + it("claimPrActuationLock fails OPEN when the atomic claim primitive itself throws (#2135)", async () => { + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async () => null, + set: async () => undefined, + claim: async () => { throw new Error("redis unavailable"); }, + }, + }); + expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); + }); + + it("REGRESSION (#2135): claimPrActuationLock uses an atomic check-and-set, so two genuinely concurrent claims for the SAME PR can never both succeed", async () => { + const env = createTestEnv({}); + const [first, second] = await Promise.all([ + claimPrActuationLock(env, "owner/act-repo", 7), + claimPrActuationLock(env, "owner/act-repo", 7), + ]); + expect([first, second].filter((claim) => claim.acquired)).toHaveLength(1); + }); + + it("REGRESSION (#2135): claimPrActuationLock calls the atomic claim primitive, not a separate get+set pair, when the cache supports it", async () => { + const calls: string[] = []; + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async () => { calls.push("get"); return null; }, + set: async () => { calls.push("set"); }, + claim: async () => { calls.push("claim"); return true; }, + releaseIfValue: async () => true, + }, + }); + expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); + expect(calls).toEqual(["claim"]); // never falls through to the racy get/set pair when claim is available + }); + + it("claimPrActuationLock returns true unconditionally when the cache has no claim() — no false exclusivity guarantee (#2135, review round 2)", async () => { + // A get-then-set pair (even with a re-read) is not a real exclusivity guarantee under concurrent load, so a + // cache without claim() now gets NO exclusivity at all rather than a fallback that only looks atomic. + const values = new Map(); + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async (key: string) => values.get(key) ?? null, + set: async (key: string, value: string) => { values.set(key, value); }, + }, + }); + expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); + expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); + }); + + it("REGRESSION (#2135, review round 2): claimPrActuationLock does not falsely claim exclusivity for two genuinely concurrent callers when the cache has no claim()", async () => { + const values = new Map(); + const yieldThenRun = (fn: () => T): Promise => new Promise((resolve) => queueMicrotask(() => resolve(fn()))); + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: (key: string) => yieldThenRun(() => values.get(key) ?? null), + set: (key: string, value: string) => yieldThenRun(() => { values.set(key, value); }), + }, + }); + const [first, second] = await Promise.all([ + claimPrActuationLock(env, "owner/act-repo", 7), + claimPrActuationLock(env, "owner/act-repo", 7), + ]); + expect([first.acquired, second.acquired]).toEqual([true, true]); + }); + + it("REGRESSION (#2129/#2135): a stale actuation-lock holder's release does not delete a successor's live lock", async () => { + // The exact race the ownership-token scheme exists to close: holder A's claim TTL lapses (or its finally + // block simply runs late), a NEW holder B claims the same key in the meantime, and then A's release finally + // runs. A blind del() would delete B's still-live lock; releaseIfValue only deletes when the caller's OWN + // token still matches what's stored, so A's late release is a safe no-op against B's key. + const env = createTestEnv({}); + const staleHolder = await claimPrActuationLock(env, "owner/act-repo", 7); + expect(staleHolder.acquired).toBe(true); + expect(staleHolder.ownerToken).toBeTruthy(); + // Simulate B's claim landing in the same key slot after A's token would have expired. + await env.SELFHOST_TRANSIENT_CACHE!.set!("pr-actuation-lock:owner/act-repo#7", "successor-token", 600); + await releasePrActuationLock(env, "owner/act-repo", 7, staleHolder.ownerToken); + expect(await env.SELFHOST_TRANSIENT_CACHE!.get!("pr-actuation-lock:owner/act-repo#7")).toBe("successor-token"); + // B's own release, with the matching token, does free the key. + await releasePrActuationLock(env, "owner/act-repo", 7, "successor-token"); + expect(await env.SELFHOST_TRANSIENT_CACHE!.get!("pr-actuation-lock:owner/act-repo#7")).toBeNull(); + }); + + it("releaseAiReviewLock and releasePrActuationLock are no-ops when ownerToken is null (nothing was actually claimed)", async () => { + const env = createTestEnv({}); + const calls: string[] = []; + env.SELFHOST_TRANSIENT_CACHE = { + get: async () => null, + set: async () => undefined, + releaseIfValue: async () => { calls.push("releaseIfValue"); return true; }, + }; + await releasePrActuationLock(env, "owner/act-repo", 7, null); + await releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", null); + expect(calls).toEqual([]); // a null token means nothing was claimed, so release must never touch the cache + }); + + it("REGRESSION: stale AI-review-lock holder releaseIfValue does not delete a successor's live lock", async () => { + const env = createTestEnv({}); + const staleHolder = await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"); + expect(staleHolder.acquired).toBe(true); + expect(staleHolder.ownerToken).toBeTruthy(); + await env.SELFHOST_TRANSIENT_CACHE!.set!("ai-review-lock:owner/agent-repo#7@sha1:block", "successor-token", 1800); + await releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", staleHolder.ownerToken); + expect(await env.SELFHOST_TRANSIENT_CACHE!.get!("ai-review-lock:owner/agent-repo#7@sha1:block")).toBe("successor-token"); + await releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", "successor-token"); + expect(await env.SELFHOST_TRANSIENT_CACHE!.get!("ai-review-lock:owner/agent-repo#7@sha1:block")).toBeNull(); + }); + + it("claimPrActuationLock fails open without exclusivity when claim() is present but releaseIfValue is absent (#3153)", async () => { + let claimed = false; + const store = new Map(); + const env = createTestEnv({ + SELFHOST_TRANSIENT_CACHE: { + get: async (key: string) => store.get(key) ?? null, + set: async (key: string, value: string) => { store.set(key, value); }, + claim: async (key: string, value: string) => { + claimed = true; + if (store.has(key)) return false; + store.set(key, value); + return true; + }, + }, + }); + const lock = await claimPrActuationLock(env, "owner/act-repo", 7); + expect(lock.acquired).toBe(true); + expect(lock.ownerToken).toBeNull(); + expect(claimed).toBe(false); + expect(store.size).toBe(0); + }); + + it("INVARIANT (#2129 per-PR lock): a maintenance pass defers when another pass already holds the PR's lock", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + let mergeCalls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.includes("/pulls/7/merge")) { + mergeCalls += 1; + return new Response(null, { status: 204 }); + } + if (url.includes("/pulls/7/reviews") && init?.method === "POST") return Response.json({ id: 1 }); + if (url.includes("/pulls/7/reviews")) return Response.json([]); + // Only the bare PR resource (no sub-path) — the more specific checks above already claimed + // /pulls/7/files, /pulls/7/merge, and /pulls/7/reviews. + if (/\/pulls\/7(\?|$)/.test(url)) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/commits/a7/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + if (url.includes(".gittensory.yml")) return new Response("Not Found", { status: 404 }); + if (url.endsWith("/check-runs") && init?.method === "POST") return Response.json({ id: 1 }); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.endsWith("/graphql")) return Response.json({ data: {} }); + return Response.json({}); + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + // Simulate a webhook pass already in-flight for this exact PR — a github-webhook:pr-refresh job's coalesce + // key never matches agent-regate-pr's, so the two would never dedup against each other pre-#2129; the + // shared per-PR actuation lock is what makes a second, independently-triggered pass defer instead of racing + // it. Pre-claims the SAME pr-actuation-lock key the draft-dodge/reopen-reclose paths use (#2129/#2135, + // review round 4) — one shared namespace, not a maintenance-only lock. + await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:owner/agent-repo#7", "1", 60); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "race-sweep", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); + + // The held lock made this pass skip its plan-and-execute critical section entirely — no mutation attempted. + expect(mergeCalls).toBe(0); + const actionAudits = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'agent.action.%'").first<{ n: number }>(); + expect(actionAudits?.n).toBe(0); + }); + + it("the sweep stamps the marker INLINE when the repo has no installation (audit-only, still converges) (#audit-sweep-fanout)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + // Configured but NOT installed (no installationId) — there is no installation to re-review with. + await upsertRepositoryFromGitHub(env, { name: "no-install", full_name: "owner/no-install", private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/no-install", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/no-install", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/no-install" }); + + expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([]); // no installation → no per-PR fan-out + const after = await env.DB.prepare("select last_regated_at from pull_requests where repo_full_name = ? and number = 7").bind("owner/no-install").first<{ last_regated_at: string | null }>(); + expect(typeof after?.last_regated_at).toBe("string"); // stamped inline so the sweep still advances + }); + + it("the sweep swallows a failing dispatch-time stamp on a no-installation repo and still completes (#audit-sweep-fanout)", async () => { + const env = createTestEnv({ JOBS: { async send() {} } as unknown as Queue }); + await upsertRepositoryFromGitHub(env, { name: "no-install", full_name: "owner/no-install", private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/no-install", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/no-install", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + const stamp = vi.spyOn(repositoriesModule, "markPullRequestsRegated").mockRejectedValueOnce(new Error("D1 write error")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/no-install" }); + + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); // the dispatch-time stamp failure is swallowed; the sweep still records its verdict + expect(errors.mock.calls.some((call) => String(call[0]).includes("sweep_mark_regated_failed"))).toBe(true); + stamp.mockRestore(); + errors.mockRestore(); + }); + + it("REGRESSION: the sweep DEFERS (re-queues, no fan-out) when the shared REST budget is below the maintenance floor (#audit-rate-headroom)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9200, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9200); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + // Low REST budget (10 ≤ 150 maintenance floor) with a future reset → maintenance must yield. Scoped to this + // repo's own installation bucket (#audit-rate-scoping) — the sweep now checks that bucket specifically. + await repositoriesModule.recordGitHubRateLimitObservation(env, { repoFullName: "owner/agent-repo", admissionKey: "installation:9200", resource: "rest", path: "/x", statusCode: 200, limitValue: 5000, remaining: 10, resetAt: "2026-05-28T02:30:00.000Z", observedAt: "2026-05-28T02:00:00.000Z" }); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([]); // no fan-out while deferred + expect(sent.some((m) => m.type === "agent-regate-sweep" && m.repoFullName === "owner/agent-repo")).toBe(true); // re-queued + const audit = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ outcome: string; metadata_json: string }>(); + expect(audit?.outcome).toBe("queued"); + expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ deferred: true }); + }); + + it("REGRESSION: a scheduled repo sweep does not fan out more per-PR regates while prior regate work is queued", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + snapshot: async () => ({ + totals: { pending: 1, processing: 0, dead: 0, due: 1 }, + byType: [{ type: "agent-regate-pr", status: "pending", count: 1, due: 1 }], + }), + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9201, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9201); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); + await upsertCheckSummary(env, { + id: "gate-backlog-7", + repoFullName: "owner/agent-repo", + pullNumber: 7, + headSha: "a7", + name: "Gittensory Orb Review Agent", + status: "completed", + conclusion: "success", + payload: {}, + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + const getRepo = vi.spyOn(repositoriesModule, "getRepository"); + const listOpen = vi.spyOn(repositoriesModule, "listOpenPullRequests"); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([]); + expect(getRepo).toHaveBeenCalledWith(env, "owner/agent-repo"); + expect(listOpen).toHaveBeenCalledWith(env, "owner/agent-repo"); + const audit = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ outcome: string; metadata_json: string }>(); + expect(audit?.outcome).toBe("queued"); + expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ deferred: true, regateBacklog: 1 }); + }); + + it("REGRESSION: a scheduled repo sweep ignores sweep rows when deciding per-PR regate backlog", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + snapshot: async () => ({ + totals: { pending: 0, processing: 1, dead: 0, due: 0 }, + byType: [{ type: "agent-regate-sweep", status: "processing", count: 1, due: 0 }], + }), + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9203, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9203); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 9, title: "PR9", state: "open", user: { login: "c" }, head: { sha: "a9" }, labels: [], body: "" }); + // Published at the current head so this is an ORDINARY (non-priority-repair) candidate -- this test is about + // backlog-row-type filtering, not the priority-repair "regate-repair:" tagging (covered separately above). + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 9, "a9"); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([ + expect.objectContaining({ + type: "agent-regate-pr", + deliveryId: "regate-sweep:owner/agent-repo#9", + repoFullName: "owner/agent-repo", + prNumber: 9, + installationId: 9203, + }), + ]); + }); + + it("INVARIANT: a scheduled repo sweep does not require queue introspection", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + JOBS: { + async send(m: import("../../src/types").JobMessage) { + sent.push(m); + }, + snapshot: async () => { + throw new Error("snapshot unavailable"); + }, + } as unknown as Queue, + }); + await upsertInstallation(env, { action: "created", installation: { id: 9202, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9202); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 8, title: "PR8", state: "open", user: { login: "c" }, head: { sha: "a8" }, labels: [], body: "" }); + // Published at the current head so this is an ORDINARY (non-priority-repair) candidate -- this test is about + // queue-introspection independence, not the priority-repair "regate-repair:" tagging (covered separately above). + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 8, "a8"); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([ + { + type: "agent-regate-pr", + deliveryId: "regate-sweep:owner/agent-repo#8", + repoFullName: "owner/agent-repo", + prNumber: 8, + installationId: 9202, + }, + ]); + }); + + it("REGRESSION: a per-PR re-gate job DEFERS (re-queues, no re-review/stamp) when the REST budget is below the maintenance floor (#audit-rate-headroom)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + // Scoped to this job's own installation bucket (#audit-rate-scoping) — installationId 9200 below. + await repositoriesModule.recordGitHubRateLimitObservation(env, { repoFullName: "owner/agent-repo", admissionKey: "installation:9200", resource: "rest", path: "/x", statusCode: 200, limitValue: 5000, remaining: 10, resetAt: "2026-05-28T02:30:00.000Z", observedAt: "2026-05-28T02:00:00.000Z" }); + const stamp = vi.spyOn(repositoriesModule, "markPullRequestsRegated"); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "regate-sweep:owner/agent-repo#7", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9200 }); + + expect(sent.filter((m) => m.type === "agent-regate-pr")).toHaveLength(1); // re-queued for after the reset + expect(stamp).not.toHaveBeenCalled(); // the per-PR job NEVER stamps the convergence marker — the sweep already did, at dispatch + stamp.mockRestore(); + }); + + it("REGRESSION: a 'regate-sweep:' per-PR job DEFERS at the maintenance floor even with headroom above the lower live floor (#selfhost-queue-liveness)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + // 100 remaining sits BELOW the 150 maintenance floor but ABOVE the 75 live floor -- isScheduledRegateSweepJob + // must route this "regate-sweep:"-prefixed job to the higher (150) floor, so it still defers here. Scoped to + // this job's own installation bucket (#audit-rate-scoping) — installationId 9200 below. + await repositoriesModule.recordGitHubRateLimitObservation(env, { repoFullName: "owner/agent-repo", admissionKey: "installation:9200", resource: "rest", path: "/x", statusCode: 200, limitValue: 5000, remaining: 100, resetAt: "2026-05-28T02:30:00.000Z", observedAt: "2026-05-28T02:00:00.000Z" }); + const stamp = vi.spyOn(repositoriesModule, "markPullRequestsRegated"); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "regate-sweep:owner/agent-repo#7", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9200 }); + + expect(sent.filter((m) => m.type === "agent-regate-pr")).toHaveLength(1); // re-queued for after the reset + expect(stamp).not.toHaveBeenCalled(); + stamp.mockRestore(); + }); + + it("REGRESSION: a non-'regate-sweep:' per-PR job (current-head trigger) does NOT defer at the maintenance floor, only at the lower live floor (#selfhost-queue-liveness)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + // Same 100-remaining observation as the sibling "regate-sweep:" test above (scoped to this job's own + // installation:9200 bucket, #audit-rate-scoping), but this deliveryId does NOT carry the "regate-sweep:" + // prefix (e.g. a repair-priority fan-out, or a real webhook-triggered re-review), so isScheduledRegateSweepJob + // is false and shouldWaitForGitHubRateLimit is called with the lower 75 floor: 100 > 75, so this job proceeds + // instead of deferring. + await repositoriesModule.recordGitHubRateLimitObservation(env, { repoFullName: "owner/agent-repo", admissionKey: "installation:9200", resource: "rest", path: "/x", statusCode: 200, limitValue: 5000, remaining: 100, resetAt: "2026-05-28T02:30:00.000Z", observedAt: "2026-05-28T02:00:00.000Z" }); + + // No stored PR row for prNumber 7 -- reReviewStoredPullRequest reaches its `getPullRequest` read (proving the + // rate-limit gate did not short-circuit it) and then returns immediately with no re-enqueue, since there is + // nothing to review. A deferral would instead re-enqueue this exact job (asserted absent below). + await processJob(env, { type: "agent-regate-pr", deliveryId: "regate-repair:owner/agent-repo#7", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9200 }); + + expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([]); // proceeded — no rate-limit re-enqueue + }); + + it("routes repo-scoped backfill jobs into resumable segment and detail processors", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(message: import("../../src/types").JobMessage) { + sent.push(message); + }, + } as unknown as Queue, + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false } }, + { kind: "raw-github", url: "fixture://registry" }, + "2026-05-25T00:00:00.000Z", + ), + ); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.github.com/graphql") { + return Response.json({ + data: { + rateLimit: { remaining: 4999, resetAt: "2026-05-25T01:00:00.000Z" }, + repository: { + issues: { totalCount: 0 }, + openPullRequests: { totalCount: 0 }, + mergedPullRequests: { totalCount: 0 }, + closedPullRequests: { totalCount: 0 }, + labels: { totalCount: 0 }, + }, + }, + }); + } + if (url.includes("/issues?") || url.includes("/labels?") || url.includes("/pulls?")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { type: "backfill-registered-repos", requestedBy: "api", repoFullName: "JSONbored/gittensory" }); + await processJob(env, { type: "backfill-repo-segment", requestedBy: "api", repoFullName: "JSONbored/gittensory", segment: "open_issues" }); + await processJob(env, { type: "backfill-pr-details", requestedBy: "api", repoFullName: "JSONbored/gittensory" }); + + expect(sent).toEqual(expect.arrayContaining([expect.objectContaining({ type: "backfill-repo-segment", repoFullName: "JSONbored/gittensory" })])); + expect(await listRepoSyncStates(env)).toEqual(expect.arrayContaining([expect.objectContaining({ repoFullName: "JSONbored/gittensory" })])); + }); + + it("covers optional queue payload branches for fanout, segment, and detail jobs", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITHUB_PUBLIC_TOKEN: "public-token", + JOBS: { + async send(message: import("../../src/types").JobMessage) { + sent.push(message); + }, + } as unknown as Queue, + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { + "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false }, + "we-promise/sure": { emission_share: 0.02, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false }, + }, + { kind: "raw-github", url: "fixture://registry" }, + "2026-05-25T00:00:00.000Z", + ), + ); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.github.com/graphql") { + return Response.json({ + data: { + rateLimit: { remaining: 4999, resetAt: "2026-05-25T01:00:00.000Z" }, + repository: { + issues: { totalCount: 0 }, + openPullRequests: { totalCount: 0 }, + mergedPullRequests: { totalCount: 0 }, + closedPullRequests: { totalCount: 0 }, + labels: { totalCount: 0 }, + }, + }, + }); + } + if (url.includes("/labels?") || url.includes("/pulls?") || url.includes("/issues?")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { type: "backfill-registered-repos", requestedBy: "api" }); + await processJob(env, { type: "backfill-repo-segment", requestedBy: "api", repoFullName: "JSONbored/gittensory", segment: "labels", mode: "resume", cursor: "2", force: true }); + await processJob(env, { type: "backfill-pr-details", requestedBy: "api", repoFullName: "JSONbored/gittensory", mode: "resume", cursor: 2 }); + + expect(sent).toEqual(expect.arrayContaining([expect.objectContaining({ type: "backfill-registered-repos", repoFullName: "JSONbored/gittensory" })])); + }); + + it("marks installation health from queued installation metadata", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", contents: "write", pull_requests: "write", issues: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }], + }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, 123); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.endsWith("/app/installations/123")) { + return Response.json({ + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { type: "refresh-installation-health", requestedBy: "test" }); + expect(await listInstallationHealth(env)).toMatchObject([{ status: "healthy", registeredInstalledCount: 1 }]); + }); + + it("syncs repositories added to and removed from an existing installation", async () => { + const env = createTestEnv(); + const installation = { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }; + await upsertInstallation(env, { + installation: { + ...installation, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "installation-repo-added", + eventName: "installation_repositories", + payload: { + action: "added", + installation: { id: 123 }, + repositories_added: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }, + }); + + expect(await getRepository(env, "JSONbored/gittensory")).toMatchObject({ isInstalled: true, installationId: 123 }); + expect(await getInstallation(env, 123)).toMatchObject({ + accountLogin: "JSONbored", + permissions: { metadata: "read", pull_requests: "read", issues: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "installation-repo-removed", + eventName: "installation_repositories", + payload: { + action: "removed", + installation: { id: 123 }, + repositories_removed: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }, + }); + + expect(await getRepository(env, "JSONbored/gittensory")).toMatchObject({ isInstalled: false, installationId: null }); + expect(await listProductUsageEvents(env, { limit: 10 })).toEqual( + expect.arrayContaining([ + expect.objectContaining({ eventName: "github_installation_repository_added", repoFullName: "/gittensory" }), + expect.objectContaining({ eventName: "github_installation_repository_removed", repoFullName: "/gittensory" }), + ]), + ); + }); + + it("does not record phantom telemetry when installation-created has no repositories (#installation-created-fallback)", async () => { + const env = createTestEnv(); + + // Case 1: neither repositories nor repository.full_name — must produce zero events (was [undefined]) + await processJob(env, { + type: "github-webhook", + deliveryId: "install-no-repos", + eventName: "installation", + payload: { + action: "created", + installation: { id: 900, account: { login: "empty-org", id: 99, type: "Organization" } }, + }, + }); + const eventsAfterEmpty = await listProductUsageEvents(env, { limit: 50 }); + expect(eventsAfterEmpty.filter((e) => e.eventName === "github_installation_created")).toHaveLength(0); + + // Case 2: repository fallback (no repositories array) — must produce exactly one event with consistent metadata + await processJob(env, { + type: "github-webhook", + deliveryId: "install-single-repo-fallback", + eventName: "installation", + payload: { + action: "created", + installation: { id: 901, account: { login: "single-org", id: 100, type: "Organization" } }, + repository: { name: "my-repo", full_name: "single-org/my-repo", private: false, owner: { login: "single-org" } }, + }, + }); + const eventsAfterSingle = await listProductUsageEvents(env, { limit: 50 }); + const createdEvents = eventsAfterSingle.filter((e) => e.eventName === "github_installation_created"); + expect(createdEvents).toHaveLength(1); + expect(createdEvents[0]).toMatchObject({ + eventName: "github_installation_created", + repoFullName: "/my-repo", + metadata: expect.objectContaining({ action: "created", repoCount: 1, truncatedRepos: 0 }), + }); + }); + + it("REGRESSION: installation-created telemetry falls back to repoFullName as the targetKey when the payload carries no installation.id", async () => { + const env = createTestEnv(); + // `handleInstallationCreatedWebhookEvent`'s own guard is `eventName === "installation" && action === "created"` + // -- unlike the sibling installation_repositories handler, it does NOT also require `installation.id`, so a + // malformed/partial delivery (no `installation` object at all) still enters the block. The per-repo + // `targetKey: payload.installation?.id ? \`installation:${id}\` : repoFullName` ternary must then take its + // `repoFullName` fallback arm instead of throwing or omitting the field. + await processJob(env, { + type: "github-webhook", + deliveryId: "install-created-no-installation-id", + eventName: "installation", + payload: { + action: "created", + repository: { name: "my-repo", full_name: "no-installation-org/my-repo", private: false, owner: { login: "no-installation-org" } }, + }, + }); + const events = await listProductUsageEvents(env, { limit: 50 }); + const created = events.filter((e) => e.eventName === "github_installation_created"); + expect(created).toHaveLength(1); + // No installation/sender on the payload -> installationActor is undefined -> no actor redaction applies, so + // both fields surface the real (unredacted) value here. + expect(created[0]).toMatchObject({ + eventName: "github_installation_created", + repoFullName: "no-installation-org/my-repo", + targetKey: "no-installation-org/my-repo", + }); + }); + + it("REGRESSION: a deployment_status webhook for an allowlisted repo re-reviews the correlated PR and short-circuits before the other wake triggers", async () => { + const env = createTestEnv({ GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory" }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + // Deliberately no stored PR #4242 -- reReviewStoredPullRequest's own `if (!pr || pr.state !== "open") return;` + // no-ops immediately, so this test stays focused on maybeCaptureOnDeploymentStatus's early-return contract + // (processGitHubWebhook must `return` right after it, never falling through to the other wake-trigger checks) + // without needing to mock the full re-review pipeline. + await processJob(env, { + type: "github-webhook", + deliveryId: "deployment-status-4242", + eventName: "deployment_status", + payload: { + installation: { id: 123 }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + deployment_status: { state: "success", environment_url: "https://preview.example.test" }, + deployment: { sha: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", ref: "feature", payload: JSON.stringify({ pr: 4242 }) }, + }, + } as never); + const stored = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("deployment-status-4242").first<{ status: string }>(); + expect(stored?.status).toBe("processed"); + }); + + it("publishes an opt-in gate without comment output, blocking a non-confirmed author normally (#gate-nonconfirmed)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + requireLinkedIssue: true, + }); + const calls = { minerList: 0, gateChecks: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && (init?.method ?? "GET") === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string; status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ name: "Gittensory Orb Review Agent", status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); + expect(body.conclusion).toBeUndefined(); + calls.gateChecks += 1; + return Response.json({ id: 900 }, { status: 201 }); + } + if (url.includes("/check-runs/900") && (init?.method ?? "GET") === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string; status?: string; conclusion?: string; output?: { title?: string } }; + // Non-confirmed author + linked-issue block + no issue → gated normally → failure (#gate-nonconfirmed). + expect(body).toMatchObject({ name: "Gittensory Orb Review Agent", status: "completed", conclusion: "failure", output: { title: "Gittensory Orb Review Agent: No linked issue detected" } }); + calls.gateChecks += 1; + return Response.json({ id: 900, html_url: "https://github.com/checks/900" }); + } + return new Response("not found", { status: 404 }); + }); + + // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code). + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-only", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 42, title: "Gate without issue", state: "open", user: { login: "contributor" }, head: { sha: "gate123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ minerList: 1, gateChecks: 2 }); + const stored = await getPullRequest(env, "JSONbored/gittensory", 42); + expect(stored?.lastPublishedSurfaceSha).toBe("gate123"); + const published = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_published") + .first<{ metadata_json: string }>(); + expect(published?.metadata_json).toContain('"publishedOutputs":["gate_check_run"]'); + const summary = await env.DB.prepare("select name, status, conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 42, "gate123") + .first<{ name: string; status: string; conclusion: string }>(); + expect(summary).toMatchObject({ + name: "Gittensory Orb Review Agent", + status: "completed", + conclusion: "failure", + }); + }); + + it("blocks under linkedIssueGateMode:block when the PR only cites an already-CLOSED issue (#unlinked-issue-guardrail-followup — the stale-link gaming case)", async () => { + // Before the fix, pr.linkedIssues.length > 0 alone satisfied this gate regardless of the cited issue's real + // state — a contributor could cite an already-closed (or fabricated) issue number to fake compliance. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + requireLinkedIssue: true, + }); + // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code) — mirrors the + // existing "publishes an opt-in gate..." test above, which needs the same manifest override for the raw + // DB setting to take effect as a live hard block. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/5") && !url.includes("/comments")) return Response.json({ number: 5, state: "closed", labels: [], assignees: [] }); + if (url.includes("/commits/gate124/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && (init?.method ?? "GET") === "POST") return Response.json({ id: 901 }, { status: 201 }); + if (url.includes("/check-runs/901") && (init?.method ?? "GET") === "PATCH") return Response.json({ id: 901, html_url: "https://github.com/checks/901" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-stale-link", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 43, title: "Fake compliance", state: "open", user: { login: "contributor" }, head: { sha: "gate124" }, labels: [], body: "Closes #5" }, + }, + }); + + const summary = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 43, "gate124") + .first<{ conclusion: string }>(); + expect(summary?.conclusion).toBe("failure"); + }); + + it("does NOT block under linkedIssueGateMode:block when the cited issue is genuinely OPEN", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + requireLinkedIssue: true, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/5") && !url.includes("/comments")) return Response.json({ number: 5, state: "open", labels: [], assignees: [] }); + if (url.includes("/commits/gate125/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && (init?.method ?? "GET") === "POST") return Response.json({ id: 902 }, { status: 201 }); + if (url.includes("/check-runs/902") && (init?.method ?? "GET") === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { conclusion?: string; output?: { title?: string } }; + expect(body.output?.title).not.toBe("Gittensory Orb Review Agent: No linked issue detected"); + return Response.json({ id: 902, html_url: "https://github.com/checks/902" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-open-link", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 44, title: "Real link", state: "open", user: { login: "contributor" }, head: { sha: "gate125" }, labels: [], body: "Closes #5" }, + }, + }); + + const summary = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 44, "gate125") + .first<{ conclusion: string }>(); + expect(summary?.conclusion).not.toBe("failure"); + }); + + it("accepts PR-body validation evidence for configured manifest test expectations", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + manifestPolicyGateMode: "block", + requireLinkedIssue: false, + typeLabelsEnabled: false, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { testExpectations: ["Run npm run test:ci."] }); + await upsertPullRequestFile(env, { + repoFullName: "JSONbored/gittensory", + pullNumber: 43, + path: "src/feature.ts", + status: "modified", + additions: 1, + deletions: 0, + changes: 1, + payload: {}, + }); + + const gatePatches: Array> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-validation/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + if (url.includes("/check-runs/901") && method === "PATCH") { + gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); + return Response.json({ id: 901, html_url: "https://github.com/checks/901" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-validation-evidence", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 43, + title: "Validated change", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate-validation" }, + labels: [], + body: "Validated with npm run test:ci.", + }, + }, + }); + + expect(gatePatches).toHaveLength(1); + expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "success" }); + expect(JSON.stringify(gatePatches[0])).not.toContain("Configured validation evidence missing"); + expect(JSON.stringify(gatePatches[0])).not.toContain("manifest_missing_tests"); + }); + + // REGRESSION (#3304): a PR body that merely MENTIONS testing without affirming it was done ("No tests + // run.") must not satisfy a configured manifest test expectation on the live webhook gate path. + it("still flags manifest_missing_tests for a PR body that only claims tests were NOT run", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + manifestPolicyGateMode: "block", + requireLinkedIssue: false, + typeLabelsEnabled: false, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { testExpectations: ["Run npm run test:ci."] }); + await upsertPullRequestFile(env, { + repoFullName: "JSONbored/gittensory", + pullNumber: 44, + path: "src/feature.ts", + status: "modified", + additions: 1, + deletions: 0, + changes: 1, + payload: {}, + }); + + const gatePatches: Array> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-no-validation/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 902 }, { status: 201 }); + if (url.includes("/check-runs/902") && method === "PATCH") { + gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); + return Response.json({ id: 902, html_url: "https://github.com/checks/902" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-no-validation-evidence", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 44, + title: "Unvalidated change", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate-no-validation" }, + labels: [], + body: "No tests run.", + }, + }, + }); + + expect(gatePatches).toHaveLength(1); + expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "failure" }); + expect(JSON.stringify(gatePatches[0])).toContain("Configured validation evidence missing"); + }); + + // #4607 (maybeApplyManifestPolicyGate extraction): buildFocusManifestGuidance can produce findings whose + // code is NOT one of the three enforceable manifest-policy codes (manifest_blocked_path / + // manifest_linked_issue_required / manifest_missing_tests) -- e.g. manifest_off_focus, when wantedPaths is + // configured and no changed path matches it. Those non-enforceable findings must be filtered out before + // ever reaching the advisory/gate, never published alongside an enforceable one from the same pass. + it("filters out a non-enforceable manifest finding (manifest_off_focus) while still surfacing an enforceable one", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + manifestPolicyGateMode: "block", + requireLinkedIssue: false, + typeLabelsEnabled: false, + }); + // wantedPaths configured + a changed file outside it produces manifest_off_focus (NOT one of the three + // enforceable codes); testExpectations configured + no evidence produces manifest_missing_tests (IS + // enforceable) -- so this single pass yields one filtered finding and one published finding. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { + wantedPaths: ["docs/"], + testExpectations: ["Run npm run test:ci."], + }); + await upsertPullRequestFile(env, { + repoFullName: "JSONbored/gittensory", + pullNumber: 45, + path: "src/feature.ts", + status: "modified", + additions: 1, + deletions: 0, + changes: 1, + payload: {}, + }); + + const gatePatches: Array> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-off-focus/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 903 }, { status: 201 }); + if (url.includes("/check-runs/903") && method === "PATCH") { + gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); + return Response.json({ id: 903, html_url: "https://github.com/checks/903" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-off-focus-filtered", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 45, + title: "Out of focus change", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate-off-focus" }, + labels: [], + body: "No tests run.", + }, + }, + }); + + expect(gatePatches).toHaveLength(1); + // The enforceable finding (manifest_missing_tests) is published... + expect(JSON.stringify(gatePatches[0])).toContain("Configured validation evidence missing"); + // ...but the non-enforceable finding (manifest_off_focus) is filtered out before it ever reaches the advisory. + expect(JSON.stringify(gatePatches[0])).not.toContain("Change is outside maintainer-wanted areas"); + expect(JSON.stringify(gatePatches[0])).not.toContain("manifest_off_focus"); + }); + + // REGRESSION (#3304): a PR with no body at all (GitHub sends `body: null` for an empty description) must + // fall back to treating validation evidence as absent, not throw or silently pass the manifest gate. + it("still flags manifest_missing_tests for a PR with a null body", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + manifestPolicyGateMode: "block", + requireLinkedIssue: false, + typeLabelsEnabled: false, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { testExpectations: ["Run npm run test:ci."] }); + await upsertPullRequestFile(env, { + repoFullName: "JSONbored/gittensory", + pullNumber: 45, + path: "src/feature.ts", + status: "modified", + additions: 1, + deletions: 0, + changes: 1, + payload: {}, + }); + + const gatePatches: Array> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-null-body/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 903 }, { status: 201 }); + if (url.includes("/check-runs/903") && method === "PATCH") { + gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); + return Response.json({ id: 903, html_url: "https://github.com/checks/903" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-null-body-evidence", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 45, + title: "No-description change", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate-null-body" }, + labels: [], + body: null, + }, + }, + }); + + expect(gatePatches).toHaveLength(1); + expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "failure" }); + expect(JSON.stringify(gatePatches[0])).toContain("Configured validation evidence missing"); + }); + + // REGRESSION (#4719 gate-review finding): passedValidationCount previously came ONLY from a PR-body + // prose match (hasValidationNote), with zero connection to the PR's actual CI results -- a fully green + // PR whose body simply doesn't happen to use a "tested"/"validated" word still tripped + // manifest_missing_tests. A fully-green live CI rollup must now ALSO count as validation evidence. + it("treats a fully-green live CI rollup as validation evidence even with no body validation note (#4719)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + manifestPolicyGateMode: "block", + requireLinkedIssue: false, + typeLabelsEnabled: false, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { testExpectations: ["Run npm run test:ci."] }); + await upsertPullRequestFile(env, { + repoFullName: "JSONbored/gittensory", + pullNumber: 46, + path: "src/feature.ts", + status: "modified", + additions: 1, + deletions: 0, + changes: 1, + payload: {}, + }); + + const gatePatches: Array> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // A single completed+successful, first-party check-run with no failing/pending statuses -- the + // live CI aggregate resolves this to ciState: "passed". + if (url.includes("/commits/gate-ci-green/check-runs")) { + return Response.json({ total_count: 1, check_runs: [{ name: "build", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + } + if (url.includes("/commits/gate-ci-green/status")) return Response.json({ statuses: [] }); + if (url.includes("/commits/gate-ci-green/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 904 }, { status: 201 }); + if (url.includes("/check-runs/904") && method === "PATCH") { + gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); + return Response.json({ id: 904, html_url: "https://github.com/checks/904" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-ci-green-evidence", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 46, + title: "CI-green change with a plain description", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate-ci-green" }, + labels: [], + body: "Fixes the checkout retry bug.", + }, + }, + }); + + expect(gatePatches).toHaveLength(1); + expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "success" }); + expect(JSON.stringify(gatePatches[0])).not.toContain("manifest_missing_tests"); + expect(JSON.stringify(gatePatches[0])).not.toContain("Configured validation evidence missing"); + }); + + // REGRESSION: review.auto_review.ignore_authors is only an AI/public-output skip. It must not + // suppress deterministic manifest policy blockers or the e2e-test-generation trigger that reads them. + it("still flags manifest_missing_tests for an ignored bot author without validation evidence", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + manifestPolicyGateMode: "block", + requireLinkedIssue: false, + typeLabelsEnabled: false, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { + testExpectations: ["Run npm run test:ci."], + review: { auto_review: { ignore_authors: ["*[bot]"] } }, + }); + await upsertPullRequestFile(env, { + repoFullName: "JSONbored/gittensory", + pullNumber: 47, + path: "README.md", + status: "modified", + additions: 1, + deletions: 1, + changes: 2, + payload: {}, + }); + + const gatePatches: Array> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-ignored-bot-blocked/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 905 }, { status: 201 }); + if (url.includes("/check-runs/905") && method === "PATCH") { + gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); + return Response.json({ id: 905, html_url: "https://github.com/checks/905" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-ignored-bot-blocked-evidence", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 47, + title: "Update README", + state: "open", + user: { login: "github-actions[bot]" }, + head: { sha: "gate-ignored-bot-blocked" }, + labels: [], + body: "Auto-generated by a workflow.", + }, + }, + }); + + expect(gatePatches).toHaveLength(1); + expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "failure" }); + expect(JSON.stringify(gatePatches[0])).toContain("Configured validation evidence missing"); + }); + + it("stamps a gate-only surface even when local Gate check-summary persistence fails", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const realPrepare = env.DB.prepare.bind(env.DB); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + env.DB.prepare = ((sql: string) => { + if (/insert\s+into\s+["`]?check_summaries["`]?/i.test(sql)) throw new Error("summary write failed"); + return realPrepare(sql); + }) as typeof env.DB.prepare; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-summary-fails/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 975 }, { status: 201 }); + if (url.includes("/check-runs/975") && method === "PATCH") return Response.json({ id: 975, html_url: "https://github.com/checks/975" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-summary-fails", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 85, title: "Gate summary fails", state: "open", user: { login: "contributor" }, head: { sha: "gate-summary-fails" }, labels: [], body: "No issue link." }, + }, + }); + + const stored = await getPullRequest(env, "JSONbored/gittensory", 85); + expect(stored?.lastPublishedSurfaceSha).toBe("gate-summary-fails"); + expect(errors.mock.calls.some((call) => String(call[0]).includes("gate_check_summary_upsert_failed"))).toBe(true); + errors.mockRestore(); + }); + + it("finalizes a permission-missing gate check through the neutral fallback before stamping the surface", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const realPrepare = env.DB.prepare.bind(env.DB); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + env.DB.prepare = ((sql: string) => { + if (/insert\s+into\s+["`]?check_summaries["`]?/i.test(sql)) throw new Error("summary write failed"); + return realPrepare(sql); + }) as typeof env.DB.prepare; + let patches = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-permission-fallback/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 976 }, { status: 201 }); + if (url.includes("/check-runs/976") && method === "PATCH") { + patches += 1; + if (patches === 1) + return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); + return Response.json({ id: 976, html_url: "https://github.com/checks/976" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-permission-fallback", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 86, title: "Gate permission fallback", state: "open", user: { login: "contributor" }, head: { sha: "gate-permission-fallback" }, labels: [], body: "No issue link." }, + }, + }); + + expect(patches).toBe(2); + const stored = await getPullRequest(env, "JSONbored/gittensory", 86); + expect(stored?.lastPublishedSurfaceSha).toBe("gate-permission-fallback"); + expect(errors.mock.calls.some((call) => String(call[0]).includes("gate_check_permission_missing"))).toBe(true); + expect(errors.mock.calls.some((call) => String(call[0]).includes("gate_check_summary_upsert_failed"))).toBe(true); + errors.mockRestore(); + }); + + it("does not stamp a permission-missing gate check when the neutral fallback cannot publish", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + let patches = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-permission-fallback-fails/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 977 }, { status: 201 }); + if (url.includes("/check-runs/977") && method === "PATCH") { + patches += 1; + if (patches === 1) + return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); + return new Response("fallback update failed", { status: 500 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-permission-fallback-fails", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 87, title: "Gate permission fallback fails", state: "open", user: { login: "contributor" }, head: { sha: "gate-permission-fallback-fails" }, labels: [], body: "No issue link." }, + }, + }); + + expect(patches).toBe(2); + const stored = await getPullRequest(env, "JSONbored/gittensory", 87); + expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); + const incomplete = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_incomplete") + .first<{ metadata_json: string }>(); + expect(incomplete?.metadata_json).toContain('"publishedOutputs":[]'); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_published") + .all(); + expect(published.results).toEqual([]); + }); + + it("suppresses public review output when the live PR head changed before publish", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + aiReviewMode: "off", + }); + let commentPosts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/stale.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const stale = true;" }]); + if (/\/pulls\/55(?:\?|$)/.test(url)) return Response.json({ number: 55, title: "Stale before publish", state: "open", user: { login: "contributor" }, head: { sha: "newsha" }, labels: [], body: "Fixes #1" }); + if (url.includes("/issues/55/comments") && method === "POST") { + commentPosts += 1; + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes("/issues/55/comments") && method === "GET") return Response.json([]); + return new Response("not found", { status: 404 }); + }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValue({ + status: "stale", + reason: "head_changed", + expectedHeadSha: "oldsha", + liveHeadSha: "newsha", + liveState: "open", + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "stale-before-public-output", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Stale before publish", state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(commentPosts).toBe(0); + const stale = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_review_stale") + .first<{ detail: string; metadata_json: string }>(); + expect(stale?.detail).toContain("PR head changed from oldsha to newsha"); + expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ + phase: "pre_public_output", + reason: "head_changed", + expectedHeadSha: "oldsha", + liveHeadSha: "newsha", + }); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_published") + .all(); + expect(published.results).toEqual([]); + }); + + it("retries unavailable live PR freshness while suppressing terminal stale review output", async () => { + const cases = [ + { + pullNumber: 59, + deliveryId: "unavailable-before-public-output", + title: "Unavailable before publish", + freshness: classifyPullRequestFreshness(undefined, "oldsha", { + unavailableSource: "pull_request_fetch", + unavailableDetail: "GitHub API failed for JSONbored/gittensory/pulls/59 (503)", + }), + expectRetry: true, + expectedDetail: "live PR state could not be verified", + expectedMetadata: { + reason: "unavailable", + expectedHeadSha: "oldsha", + liveHeadSha: null, + liveState: null, + unavailableSource: "pull_request_fetch", + unavailableDetail: "GitHub API failed for JSONbored/gittensory/pulls/59 (503)", + }, + }, + { + pullNumber: 60, + deliveryId: "head-unresolved-before-public-output", + title: "Unresolved head before publish", + freshness: classifyPullRequestFreshness( + { + state: "open", + head: {}, + }, + "oldsha", + ), + expectRetry: false, + expectedDetail: "live PR head SHA could not be verified", + expectedMetadata: { + reason: "head_unresolved", + expectedHeadSha: "oldsha", + liveHeadSha: null, + liveState: "open", + }, + }, + { + pullNumber: 61, + deliveryId: "unavailable-no-detail-before-public-output", + title: "Unavailable before publish without detail", + freshness: classifyPullRequestFreshness(undefined, "oldsha"), + expectRetry: true, + expectedDetail: "live PR state could not be verified", + expectedMetadata: { + reason: "unavailable", + expectedHeadSha: "oldsha", + liveHeadSha: null, + liveState: null, + unavailableSource: "unknown", + unavailableDetail: null, + }, + }, + ] as const; + + for (const scenario of cases) { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + aiReviewMode: "off", + }); + let commentPosts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes(`/pulls/${scenario.pullNumber}/files`)) return Response.json([{ filename: "src/stale.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const stale = true;" }]); + if (url.includes(`/issues/${scenario.pullNumber}/comments`) && method === "POST") { + commentPosts += 1; + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes(`/issues/${scenario.pullNumber}/comments`) && method === "GET") return Response.json([]); + return new Response("not found", { status: 404 }); + }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValue(scenario.freshness); + + const job = processJob(env, { + type: "github-webhook", + deliveryId: scenario.deliveryId, + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: scenario.pullNumber, title: scenario.title, state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "Fixes #1" }, + }, + }); + if (scenario.expectRetry) await expect(job).rejects.toThrow("live PR state unavailable"); + else await expect(job).resolves.toBeUndefined(); + + expect(commentPosts).toBe(0); + const stale = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_review_stale") + .first<{ detail: string; metadata_json: string }>(); + expect(stale?.detail).toContain(scenario.expectedDetail); + expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ + phase: "pre_public_output", + ...scenario.expectedMetadata, + }); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_published") + .all(); + expect(published.results).toEqual([]); + } + }); + + it("suppresses public review output for no-head reviews when the live PR is closed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + aiReviewMode: "off", + }); + let commentPosts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/61/files")) return Response.json([{ filename: "src/no-head.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const noHead = true;" }]); + if (url.includes("/issues/61/comments") && method === "POST") { + commentPosts += 1; + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes("/issues/61/comments") && method === "GET") return Response.json([]); + return new Response("not found", { status: 404 }); + }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValue(classifyPullRequestFreshness({ state: "closed", head: {} }, null)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "no-head-closed-before-public-output", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 61, title: "No head before publish", state: "open", user: { login: "contributor" }, head: {}, labels: [], body: "Fixes #1" }, + }, + }); + + expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ expectedHeadSha: null })); + expect(commentPosts).toBe(0); + const stale = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_review_stale") + .first<{ detail: string; metadata_json: string }>(); + expect(stale?.detail).toContain("PR is no longer open"); + expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ + phase: "pre_public_output", + reason: "closed", + expectedHeadSha: null, + liveHeadSha: null, + liveState: "closed", + }); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_published") + .all(); + expect(published.results).toEqual([]); + }); + + it("still suppresses stale public output when the stale audit write fails", async () => { + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + let staleAuditWrites = 0; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.pr_review_stale") { + staleAuditWrites += 1; + throw new Error("D1 audit failed"); + } + await originalRecordAuditEvent(auditEnv, event); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + aiReviewMode: "off", + }); + let commentPosts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/57/files")) return Response.json([{ filename: "src/stale.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const stale = true;" }]); + if (/\/pulls\/57(?:\?|$)/.test(url)) return Response.json({ number: 57, title: "Stale audit failure", state: "open", user: { login: "contributor" }, head: { sha: "newsha" }, labels: [], body: "Fixes #1" }); + if (url.includes("/issues/57/comments") && method === "POST") { + commentPosts += 1; + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes("/issues/57/comments") && method === "GET") return Response.json([]); + return new Response("not found", { status: 404 }); + }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValue({ + status: "stale", + reason: "head_changed", + expectedHeadSha: "oldsha", + liveHeadSha: "newsha", + liveState: "open", + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "stale-audit-failure", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 57, title: "Stale audit failure", state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "Fixes #1" }, + }, + }); + } finally { + auditSpy.mockRestore(); + } + + expect(staleAuditWrites).toBe(1); + expect(commentPosts).toBe(0); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_published") + .all(); + expect(published.results).toEqual([]); + }); + + it("finalizes the pending gate as skipped when the PR head changes after review work", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + let livePullReads = 0; + const checkBodies: Array<{ status?: string; conclusion?: string; output?: { title?: string; summary?: string } }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url === "https://api.github.com/graphql") { + return Response.json({ data: { repository: { pullRequest: { reviewThreads: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } }); + } + if (url.includes("/pulls/56/files")) return Response.json([{ filename: "src/final.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const final = true;" }]); + if (/\/pulls\/56(?:\?|$)/.test(url)) { + livePullReads += 1; + return Response.json({ + number: 56, + title: "Stale after review", + state: "open", + user: { login: "contributor" }, + head: { sha: "newsha" }, + labels: [], + body: "No issue link.", + }); + } + if (url.includes("/commits/oldsha/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/oldsha/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + if (url.includes("/check-runs") && method === "POST") { + checkBodies.push(JSON.parse(String(init?.body ?? "{}"))); + return Response.json({ id: 906 }, { status: 201 }); + } + if (url.includes("/check-runs/906") && method === "PATCH") { + checkBodies.push(JSON.parse(String(init?.body ?? "{}"))); + return Response.json({ id: 906 }); + } + return new Response("not found", { status: 404 }); + }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValue({ + status: "stale", + reason: "head_changed", + expectedHeadSha: "oldsha", + liveHeadSha: "newsha", + liveState: "open", + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "stale-after-review", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 56, title: "Stale after review", state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "No issue link." }, + }, + }); + + expect(livePullReads).toBe(0); + expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ expectedHeadSha: "oldsha" })); + expect(checkBodies).toHaveLength(2); + expect(checkBodies[0]).toMatchObject({ status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); + expect(checkBodies[1]).toMatchObject({ + status: "completed", + conclusion: "skipped", + output: { + title: "Gittensory Orb Review Agent skipped", + summary: "PR head changed from oldsha to newsha", + }, + }); + const stale = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_review_stale") + .first<{ metadata_json: string }>(); + expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ phase: "final_publish", reason: "head_changed" }); + }); + + it("still suppresses stale final output when the skipped gate check update fails", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + let patchAttempts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url === "https://api.github.com/graphql") { + return Response.json({ data: { repository: { pullRequest: { reviewThreads: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } }); + } + if (url.includes("/pulls/58/files")) return Response.json([{ filename: "src/final.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const final = true;" }]); + if (/\/pulls\/58(?:\?|$)/.test(url)) return Response.json({ number: 58, title: "Stale skip failure", state: "open", user: { login: "contributor" }, head: { sha: "newsha" }, labels: [], body: "No issue link." }); + if (url.includes("/commits/oldsha/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/oldsha/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 907 }, { status: 201 }); + if (url.includes("/check-runs/907") && method === "PATCH") { + patchAttempts += 1; + throw new Error("check-run update failed"); + } + return new Response("not found", { status: 404 }); + }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValue({ + status: "stale", + reason: "head_changed", + expectedHeadSha: "oldsha", + liveHeadSha: "newsha", + liveState: "open", + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "stale-skip-failure", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 58, title: "Stale skip failure", state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "No issue link." }, + }, + }); + + expect(patchAttempts).toBe(1); + const stale = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_review_stale") + .first<{ metadata_json: string }>(); + expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ phase: "final_publish", reason: "head_changed" }); + }); + + it("auto-maintain (#778): a blocking gate on an agent-configured repo records the changes-requested label, never a formal request_changes (dry-run)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", contents: "write", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + requireLinkedIssue: true, + autonomy: { review_state_label: "auto", request_changes: "auto" }, + agentDryRun: true, // dry-run → the actions are recorded but make no GitHub mutation + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code, as in the gate tests above). + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/gate123/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "auto-maintain", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 42, title: "No issue", state: "open", user: { login: "contributor" }, head: { sha: "gate123" }, labels: [], body: "No issue link." }, + }, + }); + + const labelAudit = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.action.label").first<{ outcome: string; metadata_json: string }>(); + expect(labelAudit?.outcome).toBe("completed"); + expect(JSON.parse(labelAudit?.metadata_json ?? "{}")).toMatchObject({ mode: "dry_run", actionClass: "label" }); + // The bot NEVER posts a formal request_changes (a blocking review strands the PR). With close NOT at an acting + // level here, a blocking contributor PR is only labeled; with close acting it would be closed. No request_changes. + const rcAudit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.request_changes").first<{ outcome: string }>(); + expect(rcAudit).toBeFalsy(); + }); + + it("auto-maintain (#778): uses hard guardrails so guarded paths cannot be merged", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + manifestPolicyGateMode: "block", + autonomy: { merge: "auto", request_changes: "auto" }, + agentDryRun: true, + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { hardGuardrailGlobs: ["migrations/**"] } }); + await upsertPullRequestFile(env, { + repoFullName: "JSONbored/gittensory", + pullNumber: 48, + path: "migrations/0099_attacker.sql", + status: "modified", + additions: 1, + deletions: 0, + changes: 1, + payload: {}, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/gate123/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "auto-maintain-hard-guardrail", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 48, + title: "Blocked migration", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate123" }, + labels: [], + body: "Closes #1", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + + const mergeCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.merge").first<{ n: number }>(); + expect(mergeCount?.n).toBe(0); // the hard guardrail prevents the auto-merge (the key assertion) + // The bot never posts a formal request_changes. With close NOT at an acting level here, the blocked PR is + // simply not merged (no blocking review); with close acting it would be closed. + const rcAudit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.request_changes").first<{ outcome: string }>(); + expect(rcAudit).toBeFalsy(); + }); + + it("refreshes pull request files for path-gated pre-merge checks on synchronize (#review-pre-merge-checks)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + autonomy: { merge: "observe", request_changes: "observe" }, + slopGateMode: "off", + mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { + review: { pre_merge_checks: [{ name: "Migration approval", require_label: "approved", when_paths: ["migrations/**"], enforce: true }] }, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 49, + title: "feat: add migration", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate125" }, + labels: [], + body: "Closes #1", + }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 49, path: "src/feature.ts", status: "modified", additions: 1, deletions: 0, changes: 1, payload: {} }); + + let pullFilesFetches = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/49/files")) { + pullFilesFetches += 1; + return Response.json([{ filename: "migrations/0099_security.sql", status: "added", additions: 3, deletions: 0, changes: 3 }]); + } + if (url.includes("/pulls/49/reviews")) return Response.json([]); + if (url.includes("/commits/gate125/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/gate125/status")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "pre-merge-refresh-sync", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 49, + title: "feat: add migration", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate125" }, + labels: [], + body: "Closes #1", + mergeable_state: "clean", + }, + }, + }); + + expect(pullFilesFetches).toBeGreaterThan(0); + expect((await listPullRequestFiles(env, "JSONbored/gittensory", 49)).map((file) => file.path)).toEqual(["migrations/0099_security.sql"]); + }); + + it("pre-merge checks (#review-pre-merge-checks): an enforced check that fails blocks the auto-merge", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "observe", request_changes: "observe" }, // evaluate + post the gate, take no merge/close action + agentDryRun: false, // so the gate check-run is actually POSTed (dry-run suppresses the write) and capturable + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + // The maintainer requires the "approved" label before merge — DETERMINISTIC, enforced. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { pre_merge_checks: [{ name: "Approved label required", require_label: "approved", enforce: true }] } }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 49, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); + + let gateConclusion: string | undefined; + let gateText = ""; + const captureGate = (body: { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }) => { + if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { + gateConclusion = body.conclusion; + gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; + } + }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) { + if (init?.body) captureGate(JSON.parse(init.body.toString())); + return Response.json({ id: 901 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "pre-merge-check-block", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 49, + title: "feat: add a feature", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate124" }, + labels: [], // missing the required "approved" label → the enforced check FAILS + body: "Closes #1", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + // The enforced pre-merge check failed → the gate check-run is a FAILURE that names the specific check. + expect(gateConclusion).toBe("failure"); + expect(gateText).toContain("Pre-merge check not satisfied: Approved label required"); + }); + + it("CLA gate (#2564): claMode: block + a missing consent phrase blocks the auto-merge (acceptance criterion)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "observe", request_changes: "observe" }, + agentDryRun: false, + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { consentPhrase: "I have read and agree to the CLA" } } }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 49, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); + + let gateConclusion: string | undefined; + let gateText = ""; + const captureGate = (body: { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }) => { + if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { + gateConclusion = body.conclusion; + gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; + } + }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) { + if (init?.body) captureGate(JSON.parse(init.body.toString())); + return Response.json({ id: 901 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "cla-gate-block", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 49, + title: "feat: add a feature", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate126" }, + labels: [], + body: "Closes #1", // missing the required CLA consent phrase → the gate FAILS + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + // The CLA consent phrase is missing → the gate check-run is a FAILURE naming the CLA finding. + expect(gateConclusion).toBe("failure"); + expect(gateText).toContain("CLA consent not confirmed"); + }); + + it("CLA gate (#2564): claMode: block + the consent phrase present in the PR body passes the gate", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "observe", request_changes: "observe" }, + agentDryRun: false, + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { consentPhrase: "I have read and agree to the CLA" } } }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 50, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); + + let gateConclusion: string | undefined; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) { + if (init?.body) { + const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string }; + if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) gateConclusion = body.conclusion; + } + return Response.json({ id: 902 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "cla-gate-pass", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 50, + title: "feat: add a feature", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate127" }, + labels: [], + body: "Closes #1\n\nI have read and agree to the CLA.", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + expect(gateConclusion).not.toBe("failure"); + }); + + it("CLA gate (#2564) is OFF by default: no manifest opt-in ⇒ a PR with no CLA consent still passes (zero behavior change)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "observe", request_changes: "observe" }, + agentDryRun: false, + // No gate.claMode manifest override — claGateMode stays undefined (the safe default). + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 51, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); + + let gateConclusion: string | undefined; + let gateText = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) { + if (init?.body) { + const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }; + if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { + gateConclusion = body.conclusion; + gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; + } + } + return Response.json({ id: 903 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "cla-gate-off-default", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 51, + title: "feat: add a feature", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate128" }, + labels: [], + body: "Closes #1", // no CLA consent anywhere — must not matter when claMode is off + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + expect(gateConclusion).not.toBe("failure"); + expect(gateText).not.toContain("CLA consent not confirmed"); + }); + + it("CLA gate (#2564): check-run-conclusion detection — a passing named CLA-bot check-run satisfies consent", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "observe", request_changes: "observe" }, + agentDryRun: false, + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + // Check-run-only config: no consentPhrase, so ONLY the named check-run's conclusion is consulted. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { checkRunName: "CLA Assistant Lite", checkRunAppSlug: "cla-assistant" } } }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 52, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); + + let gateConclusion: string | undefined; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/commits/gate129/check-runs")) { + return Response.json({ total_count: 1, check_runs: [{ id: 1, name: "CLA Assistant Lite", status: "completed", conclusion: "success", app: { slug: "cla-assistant" } }] }); + } + if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) { + if (init?.body) { + const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string }; + if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) gateConclusion = body.conclusion; + } + return Response.json({ id: 904 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "cla-gate-checkrun-pass", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 52, + title: "feat: add a feature", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate129" }, + labels: [], + body: "Closes #1", // no phrase — consent comes entirely from the check-run + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + expect(gateConclusion).not.toBe("failure"); + }); + + it("CLA gate (#2564): check-run-conclusion detection — a failing named CLA-bot check-run blocks the auto-merge", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "observe", request_changes: "observe" }, + agentDryRun: false, + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { checkRunName: "CLA Assistant Lite", checkRunAppSlug: "cla-assistant" } } }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 53, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); + + let gateConclusion: string | undefined; + let gateText = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/commits/gate130/check-runs")) { + return Response.json({ total_count: 1, check_runs: [{ id: 2, name: "CLA Assistant Lite", status: "completed", conclusion: "failure", app: { slug: "cla-assistant" } }] }); + } + if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) { + if (init?.body) { + const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }; + if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { + gateConclusion = body.conclusion; + gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; + } + } + return Response.json({ id: 905 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "cla-gate-checkrun-fail", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 53, + title: "feat: add a feature", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate130" }, + labels: [], + body: "Closes #1", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + expect(gateConclusion).toBe("failure"); + expect(gateText).toContain("CLA consent not confirmed"); + }); + + it("REGRESSION (gate finding): CLA gate (#2564) — a check-run-only config missing checkRunAppSlug BLOCKS the auto-merge instead of silently holding forever", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "observe", request_changes: "observe" }, + agentDryRun: false, + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + // Misconfigured: checkRunName set, checkRunAppSlug forgotten -- no run can ever be trusted, so the gate + // must BLOCK (not hold), even though a same-name check-run with a passing conclusion exists on the commit. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { checkRunName: "CLA Assistant Lite" } } }); + await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 54, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); + + let gateConclusion: string | undefined; + let gateText = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + // Even though a same-name check-run with a passing conclusion exists on the commit, the missing + // checkRunAppSlug means fetchNamedCheckRunConclusion never gets far enough to see it (returns null + // before any check-runs fetch) -- the gate must still see it as blocking, not "not evaluated". + if (url.includes("/commits/gate131/check-runs")) { + return Response.json({ total_count: 1, check_runs: [{ id: 3, name: "CLA Assistant Lite", status: "completed", conclusion: "success", app: { slug: "cla-assistant" } }] }); + } + if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) { + if (init?.body) { + const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }; + if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { + gateConclusion = body.conclusion; + gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; + } + } + return Response.json({ id: 906 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "cla-gate-checkrun-missing-slug", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 54, + title: "feat: add a feature", + state: "open", + user: { login: "contributor" }, + head: { sha: "gate131" }, + labels: [], + body: "Closes #1", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + expect(gateConclusion).toBe("failure"); + expect(gateText).toContain("CLA consent not confirmed"); + }); + +}); diff --git a/test/unit/queue-3.test.ts b/test/unit/queue-3.test.ts new file mode 100644 index 0000000000..1e00046bd2 --- /dev/null +++ b/test/unit/queue-3.test.ts @@ -0,0 +1,5158 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { generateKeyPairSync } from "node:crypto"; +import { clearInstallationTokenCacheForTest } from "../../src/github/app"; +import { clearReviewSuppressionCacheForTest } from "../../src/review/review-memory-wire"; +import { PR_PANEL_COMMENT_MARKER } from "../../src/github/comments"; +import * as backfillModule from "../../src/github/backfill"; +import * as rateLimitModule from "../../src/github/rate-limit"; +import * as repositoriesModule from "../../src/db/repositories"; +import * as reviewEffortModule from "../../src/review/review-effort"; +import * as repositorySettingsModule from "../../src/settings/repository-settings"; +import * as sentryModule from "../../src/selfhost/sentry"; +import { renderMetrics, resetMetrics } from "../../src/selfhost/metrics"; +import { jobCoalesceKey } from "../../src/selfhost/queue-common"; +import { + listCollisionEdges, + createAgentRun, + getCommandUsefulnessSummary, + getBurdenForecast, + getContributorEvidence, + getAgentRun, + getContributorScoringProfile, + getWebhookEvent, + getInstallation, + getLatestUpstreamRulesetSnapshot, + getPullRequest, + getPullRequestDetailSyncState, + upsertPullRequestDetailSyncState, + getRepository, + listUpstreamDriftReports, + listInstallationHealth, + listProductUsageDailyRollups, + listProductUsageEvents, + listPullRequests, + listPullRequestFiles, + listRepoSyncStates, + listSignalSnapshots, + persistSignalSnapshot, + recordGateBlockOutcome, + markGateOutcomeOverridden, + recordProductUsageEvent, + upsertAgentCommandAnswer, + upsertCheckSummary, + upsertIssueFromGitHub, + upsertRepoSyncSegment, + upsertInstallation, + updatePullRequestSlopAssessment, + upsertOfficialMinerDetection, + upsertPullRequestFile, + upsertPullRequestFromGitHub, + upsertIssueWatchSubscription, + upsertRepositoryAiKey, + upsertRepositorySettings, + upsertRepositoryFromGitHub, + putCachedAiReview, + markAiReviewPublished, + putCachedAiSlopAdvisory, + putCachedLinkedIssueSatisfaction, + recordReviewSuppression, + listReviewSuppressions, + setGlobalAgentFrozen, +} from "../../src/db/repositories"; +import { agentMaintenanceHeadMatchesGate, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors"; +import type { PullRequestRecord } from "../../src/types"; +import { aiReviewCacheInputFingerprint } from "../../src/review/ai-review-cache-input"; +import { fingerprint as reviewMemoryFingerprint } from "../../src/review/review-memory-match"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; +import * as focusManifestLoaderModule from "../../src/signals/focus-manifest-loader"; +import { normalizeRegistryPayload } from "../../src/registry/normalize"; +import { persistRegistrySnapshot } from "../../src/registry/sync"; +import { + classifyPullRequestFreshness, + fetchPullRequestFreshness, +} from "../../src/github/pr-freshness"; +import { createTestEnv } from "../helpers/d1"; +import { ISSUE_WAKE_MAX_PRS, MERGE_WAKE_MAX_PRS, SWEEP_MAX_PRS } from "../../src/settings/agent-sweep"; +import { AGENT_LABEL_PENDING_CLOSURE, DEFAULT_LINKED_ISSUE_HARD_RULES } from "../../src/review/linked-issue-hard-rules"; + +vi.mock("../../src/github/pr-freshness", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchPullRequestFreshness: vi.fn(async (_env: Env, args: { expectedHeadSha?: string | null }) => ({ + status: "current" as const, + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [] as string[], + })), + }; +}); + +// The re-gate sweep now FANS OUT the heavy re-review + marker stamp into per-PR `agent-regate-pr` jobs +// (#audit-sweep-fanout). A test asserting the re-review/stamp side effects must run the sweep AND drain the +// per-PR jobs it enqueues. Returns the captured agent-regate-pr jobs for assertions. +async function sweepAndDrainPerPr(env: Env, repoFullName: string): Promise { + const fanned: import("../../src/types").JobMessage[] = []; + const send = env.JOBS.send.bind(env.JOBS); + env.JOBS.send = (async (message: import("../../src/types").JobMessage, options?: QueueSendOptions) => { + if (message.type === "agent-regate-pr") fanned.push(message); + return send(message, options); + }) as typeof env.JOBS.send; + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName }); + env.JOBS.send = send; + for (const job of fanned) await processJob(env, job); + return fanned; +} + + +function completeSegment(repoFullName: string, segment: "labels" | "open_issues" | "open_pull_requests") { + return { + repoFullName, + segment, + status: "complete" as const, + sourceKind: "test" as const, + mode: "resume" as const, + fetchedCount: 1, + expectedCount: 1, + pageCount: 1, + completedAt: "2026-05-25T00:00:00.000Z", + warnings: [], + }; +} + +type CommandAnswerFixture = Parameters[1]; + +function commandAnswer(id: string, command: string, overrides: Partial = {}): CommandAnswerFixture { + return { + id, + repoFullName: "JSONbored/gittensory", + issueNumber: 77, + command, + requestCommentId: 7, + responseCommentId: 9001, + responseUrl: "https://github.com/JSONbored/gittensory/pull/77#issuecomment-9001", + actorKind: "maintainer" as const, + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + metadata: {}, + ...overrides, + }; +} + +function commandAnswerBody(answerId: string, command: string): string { + return [ + "", + ``, + `Command: \`@gittensory ${command}\``, + "Feedback is aggregate-only.", + ].join("\n"); +} + +function queueMinerSnapshot(login: string) { + return { + source: "gittensor_api" as const, + githubId: "123", + githubUsername: login, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + issueDiscoveryScore: 0, + issueTokenScore: 0, + issueCredibility: 1, + isIssueEligible: false, + issueEligibleRepoCount: 0, + alphaPerDay: 0, + taoPerDay: 0, + usdPerDay: 0, + totals: { + pullRequests: 3, + mergedPullRequests: 2, + openPullRequests: 1, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + }, + repositories: [], + pullRequests: [], + issueLabels: [], + }; +} + +function b64(value: string): string { + return Buffer.from(value, "utf8").toString("base64"); +} + +function withProductUsageInsertFailure(env: Env): Env { + const db = env.DB as unknown as { prepare(sql: string): unknown; batch(statements: unknown[]): Promise }; + return { + ...env, + DB: { + prepare(sql: string) { + if (sql.includes("product_usage_events")) throw new Error("product usage insert failed"); + return db.prepare.call(db, sql); + }, + batch(statements: unknown[]) { + return db.batch.call(db, statements); + }, + } as unknown as D1Database, + }; +} + +async function generatePrivateKeyPem(): Promise { + const key = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); + const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; +} + +describe("queue processors", () => { + // Freshness-SLO fixtures are dated relative to late May 2026; pin the clock so staleness windows + // stay deterministic regardless of when CI runs. + beforeEach(() => { + clearInstallationTokenCacheForTest(); + clearReviewSuppressionCacheForTest(); + vi.mocked(fetchPullRequestFreshness).mockReset(); + vi.mocked(fetchPullRequestFreshness).mockImplementation(async (_env, args) => ({ + status: "current", + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [], + })); + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-05-28T00:00:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + async function setupPlannerRepo(env: Env): Promise { + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, repository_selection: "selected", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["issues", "issue_comment"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + } + + function plannerWebhook(commentBody: string, sender: string, issueOverride?: Record): Parameters[1] { + return { + type: "github-webhook", + deliveryId: `plan-${sender}-${commentBody.length}-${issueOverride ? "pr" : "issue"}`, + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: issueOverride ?? { number: 77, title: "Add a retry to the fetch helper", state: "open", user: { login: "reporter" }, body: "The fetch helper should retry on 5xx." }, + comment: { body: commentBody, user: { login: sender, type: "User" } }, + sender: { login: sender, type: "User" }, + }, + } as unknown as Parameters[1]; + } + + it("planner (#issue-coding-plan): a maintainer @gittensory plan on an issue posts an AI plan (flag ON)", async () => { + const run = vi.fn(async () => ({ response: "## Summary\nAdd retry-on-5xx to the fetch helper.\n\n## Steps\n1. Wrap the fetch in a retry loop." })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + let postedBody: string | undefined; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer + if (url.includes("/issues/77/comments")) { + postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); + expect(run).toHaveBeenCalledTimes(1); + expect(postedBody).toContain("Gittensory implementation plan"); + expect(postedBody).toContain("Add retry-on-5xx"); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.issue_plan_generated").first<{ n: number }>(); + expect(audit?.n).toBe(1); + const usage = await env.DB.prepare("select feature, actor, status, estimated_neurons, metadata_json from ai_usage_events where feature = ?").bind("issue_plan").first<{ feature: string; actor: string; status: string; estimated_neurons: number; metadata_json: string }>(); + expect(usage?.status).toBe("ok"); + expect(usage?.actor).toBe("maintainer1"); + expect(usage?.estimated_neurons).toBeGreaterThan(0); + expect(JSON.parse(usage?.metadata_json ?? "{}")).toMatchObject({ repoFullName: "JSONbored/gittensory", issueNumber: 77 }); + }); + + it("planner: enforces the shared AI budget before calling Workers AI", async () => { + const run = vi.fn(async () => ({ response: "should not run" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI_DAILY_NEURON_BUDGET: "0", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); + expect(run).not.toHaveBeenCalled(); + const usage = await env.DB.prepare("select status from ai_usage_events where feature = ?").bind("issue_plan").first<{ status: string }>(); + expect(usage?.status).toBe("quota_exceeded"); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("no_plan_generated"); + }); + + it("planner: respects agentPaused — never spends Workers AI on a paused repo (#2257)", async () => { + const run = vi.fn(async () => ({ response: "should not run" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); + expect(run).not.toHaveBeenCalled(); // no speculative AI spend on a paused repo + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("agent_paused"); + }); + + it("planner: respects a global freeze — never spends Workers AI while the DB kill-switch is engaged (#2257)", async () => { + const run = vi.fn(async () => ({ response: "should not run" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AGENT_ACTIONS_PAUSED: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); + expect(run).not.toHaveBeenCalled(); + }); + + it("planner: respects agentDryRun — never spends Workers AI on a dry-run repo (#2257)", async () => { + const run = vi.fn(async () => ({ response: "should not run" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); + expect(run).not.toHaveBeenCalled(); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("dry_run"); + }); + + it("planner: enforces a per-actor per-repo cooldown before spending AI", async () => { + const run = vi.fn(async () => ({ response: "## Summary\nPlan." })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/77/comments")) return Response.json({ id: init?.body ? 5 : 6 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); + await processJob(env, plannerWebhook("@gittensory plan again", "maintainer1")); + expect(run).toHaveBeenCalledTimes(1); + const cooldown = await env.DB.prepare("select detail from audit_events where event_type = ? and detail = ?").bind("github_app.issue_plan_skipped", "cooldown_active").first<{ detail: string }>(); + expect(cooldown?.detail).toBe("cooldown_active"); + }); + + it("planner: flag OFF is byte-identical — @gittensory plan posts no plan and the AI is never called", async () => { + const run = vi.fn(async () => ({ response: "should not run" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "false", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + let postedPlan = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/77/comments")) { + if (init?.body && JSON.parse(init.body.toString()).body?.includes("implementation plan")) postedPlan = true; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); + expect(run).not.toHaveBeenCalled(); + expect(postedPlan).toBe(false); + }); + + it("planner: a NON-maintainer is denied — no plan is generated or posted (flag ON)", async () => { + const run = vi.fn(async () => ({ response: "should not run" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not a maintainer + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "outsider")); + expect(run).not.toHaveBeenCalled(); + const denied = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); + // Authorization now flows through the per-repo commandAuthorization policy (#21), so the skip reason is the + // policy's verdict (not the old bespoke "actor_not_maintainer"). + expect(denied?.detail).toBe("not_maintainer_or_pr_author"); + }); + + it("planner (#21): honors a per-repo commandAuthorization override that restricts `plan` to maintainers", async () => { + const run = vi.fn(async () => ({ response: "should not run" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + // Override: `plan` is maintainer-ONLY (drop the default collaborator role). + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { plan: ["maintainer"] } } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "write" }); // collaborator, not maintainer + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "collab1")); + expect(run).not.toHaveBeenCalled(); + const denied = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); + expect(denied?.detail).toBe("not_maintainer_or_pr_author"); + }); + + + it("planner: a flag-ON non-plan comment is not intercepted (the handler declines)", async () => { + const run = vi.fn(async () => ({ response: "nope" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); + await processJob(env, plannerWebhook("just a normal comment with no command", "maintainer1")); + expect(run).not.toHaveBeenCalled(); // not a plan command → maybeProcessPlanCommand returns false, no AI spend + }); + + it("planner (#22): @gittensory plan on a PR is NOT consumed — it falls through (no plan, no skip audit)", async () => { + const run = vi.fn(async () => ({ response: "nope" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + vi.stubGlobal("fetch", async () => Response.json({})); + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1", { number: 77, title: "PR not issue", state: "open", user: { login: "x" }, body: "b", pull_request: { url: "https://api.github.com/x" } })); + expect(run).not.toHaveBeenCalled(); + // Planning is issue-only; a PR-thread `plan` falls through to the mention/help path (flag-ON now matches + // flag-OFF) instead of being swallowed as a plan skip. + const planAudits = await env.DB.prepare("select count(*) as n from audit_events where event_type in (?, ?)").bind("github_app.issue_plan_skipped", "github_app.issue_plan_generated").first<{ n: number }>(); + expect(planAudits?.n).toBe(0); + }); + + it("planner: a bot-authored @gittensory plan on an issue is recorded as a classifier skip", async () => { + const run = vi.fn(async () => ({ response: "nope" })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + vi.stubGlobal("fetch", async () => Response.json({})); + await processJob(env, { + type: "github-webhook", + deliveryId: "plan-bot", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Issue", state: "open", user: { login: "reporter" }, body: "b" }, + comment: { body: "@gittensory plan", user: { login: "some-bot[bot]", type: "Bot" } }, + sender: { login: "some-bot[bot]", type: "Bot" }, + }, + } as unknown as Parameters[1]); + expect(run).not.toHaveBeenCalled(); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("unsupported_comment_action_or_bot"); + }); + + it("planner: a maintainer request that yields no plan is recorded as a skip (fail-safe)", async () => { + const run = vi.fn(async () => ({ response: " " })); // model returns nothing usable + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); + await setupPlannerRepo(env); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "write" }); // maintainer + if (url.includes("/issues/77/comments")) { + posted = true; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); + expect(posted).toBe(false); // no plan → nothing posted + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("no_plan_generated"); + }); + + it("configuration (#2168): a maintainer @gittensory configuration posts the effective resolved config", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupPlannerRepo(env); + let postedBody: string | undefined; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer + if (url.includes("/issues/77/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/77/comments") && method === "POST") { + postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory configuration", "maintainer1")); + expect(postedBody).toContain("Effective review configuration"); + expect(postedBody).toContain("Agent execution mode: **live**"); + expect(postedBody).toContain("Autonomy by action class:"); + // public-safe: never leaks a reward/trust/wallet field + expect(postedBody?.toLowerCase()).not.toMatch(/reward|wallet|hotkey|coldkey|trustscore/); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.configuration_posted").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + }); + + it.each([ + ["env pause", async (env: Env) => { (env as Env & { AGENT_ACTIONS_PAUSED: string }).AGENT_ACTIONS_PAUSED = "true"; }, "paused"], + ["repo pause", async (env: Env) => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); }, "paused"], + ["repo dry-run", async (env: Env) => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true }); }, "dry_run"], + ["DB global freeze", async (env: Env) => { await setGlobalAgentFrozen(env, true); }, "paused"], + ] as const)("configuration respects %s — never posts the effective-config comment live", async (_label, applyPause, expectedMode) => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupPlannerRepo(env); + await applyPause(env); + const calls = { commentPosts: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/77/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/77/comments") && method === "POST") { + calls.commentPosts += 1; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, plannerWebhook("@gittensory configuration", "maintainer1")); + + expect(calls.commentPosts).toBe(0); + const audit = await env.DB.prepare("select json_extract(metadata_json, '$.mode') as mode from audit_events where event_type = ?").bind("github_app.configuration_posted").first<{ mode: string }>(); + expect(audit?.mode).toBe(expectedMode); + }); + + it("configuration: a non-maintainer is denied — nothing is posted and a skip is recorded", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupPlannerRepo(env); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not a maintainer + if (url.includes("/issues/77/comments")) { + posted = true; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory configuration", "outsider")); + expect(posted).toBe(false); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.configuration_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("not_maintainer_or_pr_author"); + }); + + it("configuration: a non-configuration comment is not intercepted (the handler declines, no config audit)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupPlannerRepo(env); + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); + await processJob(env, plannerWebhook("just a normal comment, no mention", "maintainer1")); + const posted = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.configuration_posted").first(); + const skipped = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.configuration_skipped").first(); + expect(posted).toBeFalsy(); + expect(skipped).toBeFalsy(); + }); + + it("configuration: a bot-authored command is recorded as a classifier skip, never posted", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupPlannerRepo(env); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "config-bot", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "t", state: "open", user: { login: "reporter" }, body: "b" }, + comment: { body: "@gittensory configuration", user: { login: "some-bot[bot]", type: "Bot" } }, + sender: { login: "some-bot[bot]", type: "Bot" }, + }, + } as unknown as Parameters[1]); + expect(posted).toBe(false); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.configuration_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("unsupported_comment_action_or_bot"); + }); + + const pauseIssue = { number: 77, title: "Add a retry to the fetch helper", state: "open", user: { login: "reporter" }, body: "b", pull_request: { url: "https://api.github.com/repos/JSONbored/gittensory/pulls/77" } }; + async function seedPausePr(env: Env): Promise { + await setupPlannerRepo(env); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 77, title: "Add a retry to the fetch helper", state: "open", user: { login: "reporter" }, head: { sha: "h1" }, labels: [], body: "b" }); + } + // Mirrors hasAutoreviewPausedMarker's own MOST-RECENT-of-{paused,resumed} query (#2165) via a raw read, + // rather than exporting that internal helper just for tests -- same pattern the pre-existing pause tests + // already use (raw audit_events queries) instead of importing processors.ts internals. + async function isCurrentlyPaused(env: Env, repoFullName: string, prNumber: number): Promise { + const row = await env.DB.prepare( + "select event_type from audit_events where event_type in (?, ?) and target_key = ? and outcome = ? order by created_at desc, rowid desc limit 1", + ) + .bind("github_app.autoreview_paused", "github_app.autoreview_resumed", `${repoFullName}#${prNumber}`, "completed") + .first<{ event_type: string }>(); + return row?.event_type === "github_app.autoreview_paused"; + } + + it("pause (#2164): a maintainer @gittensory pause records the autoreview-paused marker and posts a public-safe confirmation", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + let postedBody: string | undefined; + const urls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + urls.push(url); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer + if (url.includes("/issues/77/comments")) { + postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory pause flaky CI, will re-enable after the fix", "maintainer1", pauseIssue)); + expect(postedBody).toContain("Auto-review paused by @maintainer1"); + expect(postedBody).toContain("Gate enforcement and the one-shot disposition are unchanged"); + expect(postedBody).toContain("flaky CI, will re-enable after the fix"); + // AUTO-REVIEW SCOPE ONLY (#2164): no Gate check-run is written and no gate-disposition audit is recorded, so the + // one-shot gate/advisory is provably untouched. + expect(urls.some((u) => u.includes("/check-runs"))).toBe(false); + const gateAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.gate_%'").first<{ n: number }>(); + expect(gateAudit?.n).toBe(0); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.autoreview_paused").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toBe("flaky CI, will re-enable after the fix"); + const usage = await env.DB.prepare("select outcome from product_usage_events where event_name = ?").bind("autoreview_paused").first<{ outcome: string }>(); + expect(usage?.outcome).toBe("completed"); + }); + + it("pause: an authorized pause with no trailing reason records the marker with a 'No reason provided.' detail", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + let postedBody: string | undefined; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/77/comments")) { + postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); + expect(postedBody).toContain("No reason provided."); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_paused").first<{ detail: string }>(); + expect(audit?.detail).toBe("No reason provided."); + }); + + it("pause: a non-maintainer is denied — nothing is posted and a denied marker is recorded (never a pause)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not a maintainer + if (url.includes("/issues/77/comments")) { + posted = true; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory pause let me in", "outsider", pauseIssue)); + expect(posted).toBe(false); + const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.autoreview_paused_denied").first<{ outcome: string }>(); + expect(denied?.outcome).toBe("denied"); + const paused = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.autoreview_paused").first(); + expect(paused).toBeFalsy(); + }); + + it("pause: a pause on a PR with no cached record is recorded as a cached_pr_missing skip, never posted", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupPlannerRepo(env); // repo + installation, but deliberately NO cached PR record + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); + expect(posted).toBe(false); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_paused_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("cached_pr_missing"); + }); + + it("pause: a bot-authored @gittensory pause is recorded as a classifier skip, never posted", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "pause-bot", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: pauseIssue, + comment: { body: "@gittensory pause", user: { login: "some-bot[bot]", type: "Bot" } }, + sender: { login: "some-bot[bot]", type: "Bot" }, + }, + } as unknown as Parameters[1]); + expect(posted).toBe(false); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_paused_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("bot_author"); + }); + + it("pause: a non-pause comment is not intercepted (the handler declines, no autoreview audit)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); + await processJob(env, plannerWebhook("just a normal comment, no mention", "maintainer1", pauseIssue)); + const paused = await env.DB.prepare("select 1 from audit_events where event_type like 'github_app.autoreview_paused%'").first(); + expect(paused).toBeFalsy(); + }); + + const reviewIssue = { number: 78, title: "Draft feature for review command", state: "open", user: { login: "reporter" }, body: "b", pull_request: { url: "https://api.github.com/repos/JSONbored/gittensory/pulls/78" } }; + async function seedReviewPr(env: Env, options: { draft?: boolean } = {}): Promise { + await setupPlannerRepo(env); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { skip_drafts: true } } }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 78, title: "Draft feature for review command", state: "open", draft: options.draft ?? true, user: { login: "reporter" }, head: { sha: "r78" }, labels: [], body: "b" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 78, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + } + function reviewCommandFetchStub(): (input: RequestInfo | URL, init?: RequestInit) => Promise { + const seen: string[] = []; + return async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + seen.push(url); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer + if (url.includes("/pulls/78/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/78")) return Response.json({ number: 78, title: "Draft feature for review command", state: "open", draft: true, user: { login: "reporter" }, head: { sha: "r78" }, labels: [], body: "b", mergeable_state: "clean" }); + if (url.includes("/commits/r78/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/r78/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + if (url.includes("/issues/78/comments") && method === "POST") return Response.json({ id: 78 }, { status: 201 }); + if (url.includes("/issues/78/comments")) return Response.json([]); + if (url.includes("/check-runs") && (method === "POST" || method === "PATCH")) return Response.json({ id: 981 }, { status: method === "POST" ? 201 : 200 }); + return Response.json({}); + }; + } + + it("review (#2163): an authorized @gittensory review posts a confirmation, dispatches a REAL re-review (proven by a live PR resync fetch inside reReviewStoredPullRequest, not just the command's own comment post), and records review_command_completed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedReviewPr(env); + let postedCommentBody: string | undefined; + let liveResyncFetched = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/issues/78/comments") && method === "POST") { + postedCommentBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; + return Response.json({ id: 78 }, { status: 201 }); + } + // reReviewStoredPullRequest's own live-head resync (#sweep-resync) GETs the PR fresh before reviewing -- + // this only happens INSIDE that function, never in the command handler's own classify/authorize/confirm + // steps, so seeing it proves the dispatch call genuinely reached the real re-review path. + if (url.endsWith("/pulls/78") && method === "GET") liveResyncFetched = true; + return reviewCommandFetchStub()(input, init); + }); + await processJob(env, plannerWebhook("@gittensory review", "maintainer1", reviewIssue)); + expect(postedCommentBody).toContain("Re-review triggered by @maintainer1"); + expect(liveResyncFetched).toBe(true); // proves the real reReviewStoredPullRequest path ran, unlike pause + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_command_completed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + const usage = await env.DB.prepare("select outcome from product_usage_events where event_name = ?").bind("review_command_completed").first<{ outcome: string }>(); + expect(usage?.outcome).toBe("completed"); + // The command itself never writes repository_settings -- it only triggers a fresh eval through the same + // path a scheduled sweep would take (#2163's hard constraint: never reimplements/flips the disposition). + const settingsRow = await env.DB.prepare("select 1 from repository_settings where repo_full_name = ?").bind("JSONbored/gittensory").first(); + expect(settingsRow).toBeFalsy(); + }); + + it("review: the 're-review' alias resolves to the same handler", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedReviewPr(env); + let postedCommentBody: string | undefined; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/issues/78/comments") && method === "POST") { + postedCommentBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; + return Response.json({ id: 78 }, { status: 201 }); + } + return reviewCommandFetchStub()(input, init); + }); + await processJob(env, plannerWebhook("@gittensory re-review", "maintainer1", reviewIssue)); + expect(postedCommentBody).toContain("Re-review triggered by @maintainer1"); + }); + + it("review: a non-maintainer/collaborator/confirmed-miner is denied — nothing posted, no re-review dispatched", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedReviewPr(env); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not authorized + if (url.includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory review", "outsider", reviewIssue)); + expect(posted).toBe(false); + const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_command_denied").first<{ outcome: string }>(); + expect(denied?.outcome).toBe("denied"); + const completed = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.review_command_completed").first(); + expect(completed).toBeFalsy(); + }); + + // REGRESSION: DEFAULT_COMMAND_AUTHORIZATION_POLICY deliberately widens "review" to confirmed_miner (a + // confirmed miner may re-trigger review on their own PR, the same self-rerun precedent as review-now). That + // requires authorizePrActionActor's needsMinerDetection: true -- an earlier version of this handler omitted + // it, so a confirmed miner's OWN PR author (not a maintainer/collaborator) was wrongly denied every time, + // since there was no other role they could match instead. + it("review: a confirmed Gittensor miner is authorized to re-review their OWN PR (not a maintainer/collaborator)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedReviewPr(env); + await upsertOfficialMinerDetection(env, "reporter", { status: "confirmed", snapshot: queueMinerSnapshot("reporter") }, 60_000); + // A confirmed miner is ALSO a confirmedContributor for the dispatched reReviewStoredPullRequest's own + // public-surface eligibility, so this pass can post a SECOND, unrelated deterministic panel comment + // alongside the review command's own confirmation -- collect every posted body rather than assuming + // the command's confirmation is the only (or the last) one. + const postedBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/collaborators/") && url.includes("/permission")) return new Response("not found", { status: 404 }); // no repo permission at all + if (url.includes("/issues/78/comments") && method === "POST") { + postedBodies.push(init?.body ? JSON.parse(init.body.toString()).body : ""); + return Response.json({ id: 78 }, { status: 201 }); + } + return reviewCommandFetchStub()(input, init); + }); + + await processJob(env, plannerWebhook("@gittensory review", "reporter", reviewIssue)); + + expect(postedBodies.some((body) => body.includes("Re-review triggered by @reporter"))).toBe(true); + const completed = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_command_completed").first<{ outcome: string }>(); + expect(completed?.outcome).toBe("completed"); + const denied = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.review_command_denied").first(); + expect(denied).toBeFalsy(); + const forceBypass = await env.DB.prepare("select 1 from audit_events where event_type = ? and target_key = ?").bind("github_app.ai_review_force_bypass", "JSONbored/gittensory#78").first(); + expect(forceBypass).toBeFalsy(); + }); + + it("review: respects agentPaused and agentDryRun without dispatching re-review", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedReviewPr(env); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); + vi.stubGlobal("fetch", reviewCommandFetchStub()); + + await processJob(env, plannerWebhook("@gittensory review", "maintainer1", reviewIssue)); + let skipped = await env.DB.prepare("select detail from audit_events where event_type = ? order by rowid desc limit 1").bind("github_app.review_command_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("agent_paused"); + let completed = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.review_command_completed").first(); + expect(completed).toBeFalsy(); + + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: false, agentDryRun: true }); + await processJob(env, plannerWebhook("@gittensory review", "maintainer1", reviewIssue)); + skipped = await env.DB.prepare("select detail from audit_events where event_type = ? order by rowid desc limit 1").bind("github_app.review_command_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("dry_run"); + completed = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.review_command_completed").first(); + expect(completed).toBeFalsy(); + }); + + it("review: a review command on a PR with no cached record is recorded as a cached_pr_missing skip, never posted", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupPlannerRepo(env); // repo + installation, but deliberately NO cached PR record + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory review", "maintainer1", reviewIssue)); + expect(posted).toBe(false); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.review_command_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("cached_pr_missing"); + }); + + it("review: a bot-authored @gittensory review is recorded as a classifier skip, never posted", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedReviewPr(env); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "review-bot", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: reviewIssue, + comment: { body: "@gittensory review", user: { login: "some-bot[bot]", type: "Bot" } }, + sender: { login: "some-bot[bot]", type: "Bot" }, + }, + } as unknown as Parameters[1]); + expect(posted).toBe(false); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.review_command_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("bot_author"); + }); + + it("resume (#2165): an authorized @gittensory resume clears an earlier pause and posts a public-safe confirmation", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/77/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 5 }, { status: 201 }); + if (url.includes("/issues/77/comments")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + // Pause first, matching real usage: a resume without a prior pause is still valid (idempotent), but this + // proves the SUPERSEDE behavior, not just that resume can run standalone. + await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); + expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(true); + + let postedBody: string | undefined; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/77/comments")) { + postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; + return Response.json({ id: 6 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory resume", "maintainer1", pauseIssue)); + expect(postedBody).toContain("Auto-review resumed by @maintainer1"); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.autoreview_resumed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + // The core bug fix (#2165): hasAutoreviewPausedMarker now reads the MOST RECENT of {paused, resumed}, so + // resume actually supersedes the earlier pause instead of silently no-opping forever. + expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(false); + }); + + it("resume: a LATER pause after a resume still re-pauses correctly (ordering, not just existence)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + const adminFetch = (): ((input: RequestInfo | URL, init?: RequestInit) => Promise) => async (input, init) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/77/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 5 }, { status: 201 }); + if (url.includes("/issues/77/comments")) return Response.json([]); + return new Response("not found", { status: 404 }); + }; + vi.stubGlobal("fetch", adminFetch()); + await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); + expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(true); + vi.stubGlobal("fetch", adminFetch()); + await processJob(env, plannerWebhook("@gittensory resume", "maintainer1", pauseIssue)); + expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(false); + vi.stubGlobal("fetch", adminFetch()); + await processJob(env, plannerWebhook("@gittensory pause again", "maintainer1", pauseIssue)); + expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(true); + }); + + it("resume: a non-maintainer/collaborator is denied — nothing posted and the pause marker is untouched", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/77/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 5 }, { status: 201 }); + if (url.includes("/issues/77/comments")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not authorized + if (url.includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory resume", "outsider", pauseIssue)); + expect(posted).toBe(false); + const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.autoreview_resumed_denied").first<{ outcome: string }>(); + expect(denied?.outcome).toBe("denied"); + expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(true); // still paused + }); + + it("resume: a resume on a PR with no cached record is recorded as a cached_pr_missing skip, never posted", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupPlannerRepo(env); // repo + installation, but deliberately NO cached PR record + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@gittensory resume", "maintainer1", pauseIssue)); + expect(posted).toBe(false); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_resumed_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("cached_pr_missing"); + }); + + it("resume: a bot-authored @gittensory resume is recorded as a classifier skip, never posted", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resume-bot", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: pauseIssue, + comment: { body: "@gittensory resume", user: { login: "some-bot[bot]", type: "Bot" } }, + sender: { login: "some-bot[bot]", type: "Bot" }, + }, + } as unknown as Parameters[1]); + expect(posted).toBe(false); + const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_resumed_skipped").first<{ detail: string }>(); + expect(skip?.detail).toBe("bot_author"); + }); + + it("REGRESSION (#audit-draft-maintenance): a clean DRAFT PR is never auto-merged/approved/closed (drafts are WIP)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + action: "created", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + target_type: "User", + repository_selection: "all", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // Clean, mergeable, approved, green CI + merge:auto + close:auto + approve:auto — a NON-draft here would be + // auto-acted. The ONLY thing that must stop it is the draft guard in maybeRunAgentMaintenance. + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "auto", approve: "auto", close: "auto" }, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/draft1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/draft1/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) return Response.json({ id: 901 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "draft-no-maintenance", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 49, + title: "Work in progress", + state: "open", + draft: true, + user: { login: "contributor" }, + head: { sha: "draft1" }, + labels: [], + body: "Closes #1", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + + // No terminal maintenance action of ANY class fires on a draft. + const acted = await env.DB.prepare("select count(*) as n from audit_events where event_type in ('agent.action.merge','agent.action.approve','agent.action.close')").first<{ n: number }>(); + expect(acted?.n).toBe(0); + }); + + it("blacklist (#1425): a banned author's PR is labeled + closed deterministically with NO AI call and no merit merge", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "n/a", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + // The banned login is per-repo DB config; the label is the configurable `.gittensory.yml` value below — + // nothing is hard-coded. + contributorBlacklist: [{ login: "baduser", reason: "plagiarism" }], + }); + // The label is configurable via `.gittensory.yml` (default "slop"); set a custom one to prove it's not hardcoded. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { blacklistLabel: "spam" } }, "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(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "baduser" }, head: { sha: "bl55" }, mergeable_state: "clean" }); + if (url.includes("/commits/bl55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/bl55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/55/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "blacklist-close", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Banned author PR", state: "open", user: { login: "baduser" }, head: { sha: "bl55" }, labels: [], body: "Closes #1", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + // Deterministic gate: closed + labeled (with the configured label), and the AI was NEVER called. + expect(aiCalls).toBe(0); + expect(seen.closed).toBe(true); + expect(seen.labels).toContain("spam"); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + // No merit merge despite a clean+green+approved PR (the blacklist short-circuits ahead of merit). + const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); + expect(mergeAudit?.n).toBe(0); + // The close comment is public-safe and explains the block. + expect(seen.comments.some((c) => c.includes("blocked from contributing"))).toBe(true); + }); + + it("screenshot-table gate (#2006): an in-scope contributor PR missing a before/after table is closed deterministically with NO AI call and no merit merge", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "n/a", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + }); + // Scoped to the `visual` label only, config-as-code, nothing hardcoded — mirrors the blacklistLabel test above. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { screenshotTableGate: { enabled: true, whenLabels: ["visual"] } } }, "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(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/56/files")) return Response.json([{ filename: "apps/ui/src/App.tsx", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/56/reviews")) return Response.json([]); + if (url.includes("/pulls/56/commits")) return Response.json([]); + if (url.endsWith("/pulls/56") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 56, state: "closed" }); } + if (url.endsWith("/pulls/56")) return Response.json({ number: 56, state: "open", user: { login: "visual-contributor" }, head: { sha: "vis56" }, mergeable_state: "clean" }); + if (url.includes("/commits/vis56/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/vis56/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/56/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/56/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/56/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/issues/56/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "screenshot-table-close", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 56, title: "New button color", state: "open", user: { login: "visual-contributor" }, head: { sha: "vis56" }, labels: [{ name: "visual" }], body: "Changed the button color. Closes #1", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + // Deterministic gate: closed, and the AI was NEVER called for the disposition. + expect(aiCalls).toBe(0); + expect(seen.closed).toBe(true); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + // No merit merge despite a clean+green+approved PR (the screenshot-table gate short-circuits ahead of merit). + const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); + expect(mergeAudit?.n).toBe(0); + // The close comment explains the missing table. + expect(seen.comments.some((c) => c.includes("before/after screenshot table"))).toBe(true); + }); + + it("screenshot-table gate (#2006): an in-scope PR WITH a valid before/after table is NOT closed by the gate (no false-positive)", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto", merge: "auto" }, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { screenshotTableGate: { enabled: true, whenLabels: ["visual"] } } }, "repo_file"); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/57/files")) return Response.json([{ filename: "apps/ui/src/App.tsx", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/57/reviews")) return Response.json([]); + if (url.includes("/pulls/57/commits")) return Response.json([]); + if (url.endsWith("/pulls/57") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 57, state: "closed" }); } + if (url.endsWith("/pulls/57")) return Response.json({ number: 57, state: "open", user: { login: "visual-contributor" }, head: { sha: "vis57" }, mergeable_state: "clean" }); + if (url.includes("/commits/vis57/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/vis57/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + if (url.includes("/issues/57/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/57/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "screenshot-table-pass", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 57, + title: "New button color", + state: "open", + user: { login: "visual-contributor" }, + head: { sha: "vis57" }, + labels: [{ name: "visual" }], + body: "Changed the button color.\n\n| Before | After |\n| --- | --- |\n| ![before](https://x/before.png) | ![after](https://x/after.png) |\n\nCloses #1", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + + // The valid before/after table means the deterministic gate never matches — no close of any kind fires. + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBe(0); + }); + + // #4110: same in-scope, NO-body-table fixture as the "closed deterministically" test above (a hand-authored + // table would normally be the ONLY way to avoid the close) -- the ONLY difference is that this PR ALSO + // touches a web-visible route file with a real, resolvable preview deploy. Proves the marker + // (markPullRequestVisualCaptureSatisfied) is READ BACK correctly (evaluateScreenshotTableGate's + // botCaptureSatisfied) without a hand-authored table. + // + // #4136: isPersistedShotUrl now requires a real `key=` R2 URL, which only a genuine Browser Rendering pass + // can produce (env.BROWSER is unavailable in this unit-test environment, so buildCapture always falls back + // to a placeholder here -- covered separately by test/unit/visual-shot.test.ts's own captureShot mocking). + // Rather than mock a full headless-browser launch just to exercise this gate-read-back assertion, this + // seeds the marker the SAME way production does: markPullRequestVisualCaptureSatisfied is called by an + // EARLIER pass (a `synchronize` capture) at this exact head SHA, before the webhook under test runs. This + // is not a weaker test of the real behavior -- capture and gate evaluation routinely happen on different + // webhook deliveries in production (buildCapture runs on `synchronize`; the maintenance pass that reads the + // marker back can fire later, e.g. a re-gate sweep) -- and it still fully proves the read-back half of the + // #4110 gate: upsertPullRequestFromGitHub's own onConflict clause never touches visualCaptureSatisfiedSha + // (see its own comment), so the marker survives this webhook's PR upsert untouched, exactly as it would + // survive any later webhook in production. + it("screenshot-table gate (#4110): a persisted bot capture from an earlier pass satisfies the gate, no body table needed", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", + GITTENSORY_REVIEW_SCREENSHOTS: "true", + }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto", label: "auto" }, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { screenshotTableGate: { enabled: true, whenLabels: ["visual"] } } }, "repo_file"); + // Simulates an earlier `synchronize` pass whose real (Browser Rendering) capture already succeeded at + // this head SHA and persisted the marker -- see the test doc comment above. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 58, + title: "Update the app index route", + state: "open", + user: { login: "visual-contributor" }, + head: { sha: "vis58" }, + labels: [{ name: "visual" }], + body: "Changed the route layout, no table here.", + }); + await repositoriesModule.markPullRequestVisualCaptureSatisfied(env, "JSONbored/gittensory", 58, "vis58"); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // A web-visible route file (isVisualPath) — this is what makes screenshotsAllowed's file-touch gate open + // and buildCapture actually run, on TOP of the no-body-table screenshotTableGate scope match (label). + if (url.includes("/pulls/58/files")) return Response.json([{ filename: "apps/gittensory-ui/src/routes/app.index.tsx", status: "modified", additions: 5, deletions: 1, changes: 6, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/58/reviews")) return Response.json([]); + if (url.includes("/pulls/58/commits")) return Response.json([]); + if (url.endsWith("/pulls/58") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 58, state: "closed" }); } + if (url.endsWith("/pulls/58")) return Response.json({ number: 58, state: "open", user: { login: "visual-contributor" }, head: { sha: "vis58" }, mergeable_state: "clean" }); + // Deployments API: none found -> buildCapture falls through to findPreviewUrlFromChecks below. + if (url.includes("/deployments?")) return Response.json([]); + // Combined status: empty statuses[] (byte-identical to the sibling "closed deterministically" fixture's + // CI stub) -- findPreviewUrlFromChecks' status lookup finds nothing here and falls through to check-runs. + if (url.includes("/commits/vis58/status")) return Response.json({ state: "success", statuses: [] }); + // A completed, successful check-run whose details_url is a real workers.dev preview link -- + // findPreviewUrlFromChecks' SECOND lookup resolves it, and reduceLiveCiAggregate reads it as an ordinary + // green check (no pending/failing signal), so CI still evaluates "passed". + if (url.includes("/commits/vis58/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "preview-deploy", status: "completed", conclusion: "success", details_url: "https://pr-58-preview.workers.dev" }] }); + // Check-suite hardening: reduceLiveCiAggregate only certifies a commit settled once it can ALSO read the + // check-suites (a non-empty check-runs list makes it fetch this as a backstop) -- an unstubbed 404 here + // would fail CLOSED to "pending" and defer the whole review before it ever reaches the publish/maintain + // pass. An empty list means nothing is still running. + if (url.includes("/commits/vis58/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/issues/58/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/58/comments")) return Response.json([]); + // The unified-comment path also creates/patches the "Gittensory Orb Review Agent" check run and applies + // the title-derived type label -- neither is under test here, but both must resolve so the review + // completes normally instead of throwing on an unstubbed 404. + if (url.endsWith("/labels") && method === "POST") return Response.json([]); + if (url.endsWith("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "screenshot-table-bot-capture", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 58, + title: "Update the app index route", + state: "open", + user: { login: "visual-contributor" }, + head: { sha: "vis58" }, + labels: [{ name: "visual" }], + body: "Changed the route layout, no table here.", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + + // The bot's own capture already proved the change visually -- no close, despite no body table at all. + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBe(0); + // The marker persisted and round-trips through toPullRequestRecordFromRow. + const stored = await getPullRequest(env, "JSONbored/gittensory", 58); + expect(stored?.visualCaptureSatisfiedSha).toBe("vis58"); + }); + + // #4110 fail-safe: same fixture as the sibling "satisfies the gate on its own" test above (successful capture, + // in-scope, no body table), except the persistence write itself fails. Proves (1) the write failure never + // throws / never blocks the rest of the review (the marker write is wrapped in its own .catch), and (2) with + // NOTHING persisted, the screenshot-table gate correctly falls back to requiring a body table -- so this + // particular PR IS closed, unlike its sibling. Together the two tests pin both sides of the write's outcome. + it("screenshot-table gate (#4110): a failed visual-capture-satisfied write is swallowed (fail-safe) -- the gate falls back to requiring a body table", async () => { + const markSpy = vi.spyOn(repositoriesModule, "markPullRequestVisualCaptureSatisfied").mockRejectedValueOnce(new Error("D1 write failed")); + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", + GITTENSORY_REVIEW_SCREENSHOTS: "true", + }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto", label: "auto" }, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { screenshotTableGate: { enabled: true, whenLabels: ["visual"] } } }, "repo_file"); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/59/files")) return Response.json([{ filename: "apps/gittensory-ui/src/routes/app.index.tsx", status: "modified", additions: 5, deletions: 1, changes: 6, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/59/reviews")) return Response.json([]); + if (url.includes("/pulls/59/commits")) return Response.json([]); + if (url.endsWith("/pulls/59") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 59, state: "closed" }); } + if (url.endsWith("/pulls/59")) return Response.json({ number: 59, state: "open", user: { login: "visual-contributor" }, head: { sha: "vis59" }, mergeable_state: "clean" }); + if (url.includes("/deployments?")) return Response.json([]); + if (url.includes("/commits/vis59/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/commits/vis59/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "preview-deploy", status: "completed", conclusion: "success", details_url: "https://pr-59-preview.workers.dev" }] }); + if (url.includes("/commits/vis59/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/issues/59/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/59/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/59/comments")) return Response.json([]); + if (url.endsWith("/labels") && method === "POST") return Response.json([]); + if (url.endsWith("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "screenshot-table-bot-capture-write-fail", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 59, + title: "Update the app index route", + state: "open", + user: { login: "visual-contributor" }, + head: { sha: "vis59" }, + labels: [{ name: "visual" }], + body: "Changed the route layout, no table here.", + mergeable_state: "clean", + reviewDecision: "APPROVED", + }, + }, + }); + } finally { + markSpy.mockRestore(); + } + + // The write failure never throws / never blocks the review -- but with nothing persisted, the gate has no + // bot-capture evidence and falls back to its ordinary no-table close. + expect(seen.closed).toBe(true); + const stored = await getPullRequest(env, "JSONbored/gittensory", 59); + expect(stored?.visualCaptureSatisfiedSha).toBeNull(); + }); + + describe("live migrations/** collision recheck (#2550)", () => { + // Full merge-eligible stub set (clean + green + approved), reused across scenarios — a positive test proves + // the collision hold actually suppresses what would otherwise merge; a negative test proves the check + // correctly stays out of the way. `liveTree` is the live git/trees response for `main` (the collision + // source of truth); `seen.treeCalls` counts how many times it was fetched, so the "no latency for a + // non-migrations PR" and "off by default" requirements are directly assertable, not just inferred. + function stubMigrationRecheckFetch(prNumber: number, changedFile: { filename: string; status: string } | Array<{ filename: string; status: string }>, liveTree: Array<{ type: string; path: string }> | "error", seen: { closed: boolean; merged: boolean; labels: string[]; comments: string[]; treeCalls: number }) { + const changedFiles = Array.isArray(changedFile) ? changedFile : [changedFile]; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes(`/git/trees/main`)) { + seen.treeCalls += 1; + if (liveTree === "error") return new Response("not found", { status: 404 }); + return Response.json({ tree: liveTree }); + } + if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes(`/pulls/${prNumber}/`)) { + return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); + } + if (url.includes(`/pulls/${prNumber}/files`)) return Response.json(changedFiles.map((f) => ({ ...f, additions: 5, deletions: 0, changes: 5, patch: "@@\n+ALTER TABLE t ADD COLUMN c TEXT;" }))); + if (url.includes(`/commits/sha1/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes(`/commits/sha1/status`)) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); + if (url.includes(`/commits/sha1/check-suites`)) return Response.json({ check_suites: [] }); + if (url.includes("/branches/")) return Response.json({ contexts: [] }); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes(`/pulls/${prNumber}/merge`) && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true, sha: "merged-sha1" }); + } + if (url.includes(`/pulls/${prNumber}`) && method === "PATCH") { + seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; + return Response.json({ number: prNumber, state: "closed" }); + } + if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); // repo-level label creation (createMissingLabel probe) + if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { + seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + return Response.json({}); + }); + } + + async function seedMigrationRecheckRepo(env: Env, prNumber: number, opts: { premergeContentRecheck?: boolean } = {}) { + await upsertInstallation(env, { + installation: { id: 123, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { contents: "write", pull_requests: "write", issues: "write" }, events: [] }, + }); + await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: { merge: "auto", review_state_label: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + if (opts.premergeContentRecheck !== undefined) { + await upsertRepoFocusManifest(env, "owner/repo", { gate: { premergeContentRecheck: opts.premergeContentRecheck } }); + } + await upsertPullRequestFromGitHub(env, "owner/repo", { number: prNumber, title: "Migration PR", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main" }, labels: [], body: "" }); + } + + it("holds a would-otherwise-merge PR when the live base has a colliding migration number, with the distinct label + rebase comment", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 60, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + stubMigrationRecheckFetch(60, { filename: "migrations/0099_a.sql", status: "added" }, [{ type: "blob", path: "migrations/0099_b.sql" }], seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-collision-hold", repoFullName: "owner/repo", prNumber: 60, installationId: 123 }); + + expect(seen.merged).toBe(false); + expect(seen.closed).toBe(false); // held, never closed — this is a hold, not a close + expect(seen.labels).toContain("migration-collision"); + expect(seen.comments.some((c) => c.includes("rebase") && c.includes("0099"))).toBe(true); + expect(seen.treeCalls).toBe(1); + }); + + it("does not hold when the base has no colliding number — merges normally", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 61, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + stubMigrationRecheckFetch(61, { filename: "migrations/0099_a.sql", status: "added" }, [{ type: "blob", path: "migrations/0050_unrelated.sql" }], seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-no-collision", repoFullName: "owner/repo", prNumber: 61, installationId: 123 }); + + expect(seen.merged).toBe(true); + expect(seen.labels).not.toContain("migration-collision"); + expect(seen.treeCalls).toBe(1); + }); + + it("pays zero latency (never fetches the live tree) for a PR that does not touch migrations/**, even with the recheck enabled", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 62, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + stubMigrationRecheckFetch(62, { filename: "src/index.ts", status: "modified" }, [{ type: "blob", path: "migrations/0099_b.sql" }], seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-not-touched", repoFullName: "owner/repo", prNumber: 62, installationId: 123 }); + + expect(seen.treeCalls).toBe(0); // path-gated — never even attempted the live fetch + expect(seen.merged).toBe(true); + }); + + it("is off by default — never fetches the live tree even for a migrations/**-touching PR when unconfigured", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 63); // premergeContentRecheck left unset — defaults off + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + stubMigrationRecheckFetch(63, { filename: "migrations/0099_a.sql", status: "added" }, [{ type: "blob", path: "migrations/0099_b.sql" }], seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-recheck-off", repoFullName: "owner/repo", prNumber: 63, installationId: 123 }); + + expect(seen.treeCalls).toBe(0); + expect(seen.merged).toBe(true); // a live collision exists but the feature is off — merges anyway (opt-in) + }); + + it("fails OPEN (merges normally) when the live tree fetch errors — never holds a PR on inconclusive data", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 64, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + stubMigrationRecheckFetch(64, { filename: "migrations/0099_a.sql", status: "added" }, "error", seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-fetch-error", repoFullName: "owner/repo", prNumber: 64, installationId: 123 }); + + expect(seen.treeCalls).toBe(1); + expect(seen.merged).toBe(true); + expect(seen.labels).not.toContain("migration-collision"); + }); + + it("fails OPEN (never fetches the live tree) when the PR has no resolvable base ref", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { contents: "write", pull_requests: "write", issues: "write" }, events: [] }, + }); + // No default_branch on the repo record AND no base.ref on the PR record — baseRef resolves to undefined. + await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: { merge: "auto", review_state_label: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertRepoFocusManifest(env, "owner/repo", { gate: { premergeContentRecheck: true } }); + await upsertPullRequestFromGitHub(env, "owner/repo", { number: 65, title: "No base ref", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, labels: [], body: "" }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + stubMigrationRecheckFetch(65, { filename: "migrations/0099_a.sql", status: "added" }, [{ type: "blob", path: "migrations/0099_b.sql" }], seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-no-base-ref", repoFullName: "owner/repo", prNumber: 65, installationId: 123 }); + + expect(seen.treeCalls).toBe(0); // no live target to compare against — never even attempted the fetch + expect(seen.merged).toBe(true); + expect(seen.labels).not.toContain("migration-collision"); + }); + + it("REGRESSION: is deliberately UNCACHED — a live tree that changes between two consecutive maintenance passes is picked up fresh, never served stale", async () => { + // The exact race a caching layer would reintroduce: a sibling PR (not modeled directly here — simulated + // by the live tree response CHANGING between the two fetches, the same effect a sibling merge has) adds + // a colliding migration file in the window between two maintenance passes on the SAME PR. A cache keyed + // by repo+baseRef would serve the first (pre-collision) snapshot on the second pass and miss the + // collision entirely — this asserts both passes fetch fresh and the second one correctly detects it. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 66, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + let liveTree: Array<{ type: string; path: string }> = []; // pass 1: main has nothing colliding yet + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes("/git/trees/main")) { + seen.treeCalls += 1; + return Response.json({ tree: liveTree }); + } + if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/66/")) { + return Response.json({ number: 66, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); + } + if (url.includes("/pulls/66/files")) return Response.json([{ filename: "migrations/0099_a.sql", status: "added", additions: 5, deletions: 0, changes: 5, patch: "@@\n+ALTER TABLE t ADD COLUMN c TEXT;" }]); + if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); + if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/branches/")) return Response.json({ contexts: [] }); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/pulls/66/merge") && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true, sha: "merged-sha1" }); + } + if (url.includes("/issues/66/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/66/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes("/issues/66/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-fresh-pass-1", repoFullName: "owner/repo", prNumber: 66, installationId: 123 }); + expect(seen.merged).toBe(true); // pass 1: no collision yet — merges + + // Between passes, a sibling PR merges its own colliding 0099 file — main's live tree now has it. + liveTree = [{ type: "blob", path: "migrations/0099_b.sql" }]; + seen.merged = false; + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-fresh-pass-2", repoFullName: "owner/repo", prNumber: 66, installationId: 123 }); + + expect(seen.treeCalls).toBe(2); // every pass fetches fresh — no cache could ever mask the change + expect(seen.labels).toContain("migration-collision"); + expect(seen.merged).toBe(false); // pass 2 correctly catches the now-live collision, never stale-served + }); + + it("does NOT hold for a pre-existing collision between two OTHER files unrelated to this PR's own migration number", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 67, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + // main already has a real collision at 0050 (two unrelated, already-merged files) — nothing to do with + // this PR's own migration at 0099. The prNumbers scoping must exclude it: main is already broken by + // someone else's mistake, but that must not hold an unrelated third PR. + stubMigrationRecheckFetch(67, { filename: "migrations/0099_a.sql", status: "added" }, [ + { type: "blob", path: "migrations/0050_x.sql" }, + { type: "blob", path: "migrations/0050_y.sql" }, + ], seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-unrelated-collision", repoFullName: "owner/repo", prNumber: 67, installationId: 123 }); + + expect(seen.merged).toBe(true); + expect(seen.labels).not.toContain("migration-collision"); + }); + + it("holds and reports every colliding number when a PR touches two migration files that each independently collide", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 68, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + stubMigrationRecheckFetch( + 68, + [ + { filename: "migrations/0098_a.sql", status: "added" }, + { filename: "migrations/0099_a.sql", status: "added" }, + ], + [ + { type: "blob", path: "migrations/0098_b.sql" }, + { type: "blob", path: "migrations/0099_b.sql" }, + ], + seen, + ); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-multi-collision", repoFullName: "owner/repo", prNumber: 68, installationId: 123 }); + + expect(seen.merged).toBe(false); + expect(seen.labels).toContain("migration-collision"); + expect(seen.comments.some((c) => c.includes("0098") && c.includes("0099"))).toBe(true); + }); + + it("does not hold when the live base already contains one of the real grandfathered duplicate pairs, unrelated to this PR's own migration number", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 69, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + // The real, already-shipped 0090 grandfathered pair (see KNOWN_MIGRATION_DUPLICATES) is present on main — + // this must never trigger a hold for an unrelated PR touching a different number. + stubMigrationRecheckFetch(69, { filename: "migrations/0099_a.sql", status: "added" }, [ + { type: "blob", path: "migrations/0090_contributor_cap_label.sql" }, + { type: "blob", path: "migrations/0090_pull_request_detail_sync_head_sha.sql" }, + ], seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-grandfathered", repoFullName: "owner/repo", prNumber: 69, installationId: 123 }); + + expect(seen.merged).toBe(true); + expect(seen.labels).not.toContain("migration-collision"); + }); + + it("REGRESSION: renaming this PR's own not-yet-merged migration file (e.g. a typo fix, same number) does NOT self-collide", async () => { + // Before the fix, prMigrationFilenames was derived from changedPathsForGuardrail's collapsed set, which + // includes BOTH a renamed file's old and new name — counting one logical file as two and self-colliding. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 70, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes("/git/trees/main")) { + seen.treeCalls += 1; + return Response.json({ tree: [] }); // empty live base — nothing else to collide with + } + if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/70/")) { + return Response.json({ number: 70, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); + } + // A single GitHub PR-files entry for a rename: status="renamed", filename=new name, previous_filename=old name. + if (url.includes("/pulls/70/files")) return Response.json([{ filename: "migrations/0099_add_column.sql", previous_filename: "migrations/0099_add_colum.sql", status: "renamed", additions: 1, deletions: 1, changes: 2, patch: "@@\n rename" }]); + if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); + if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/branches/")) return Response.json({ contexts: [] }); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/pulls/70/merge") && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true, sha: "merged-sha1" }); + } + if (url.includes("/issues/70/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/70/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes("/issues/70/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-rename-self", repoFullName: "owner/repo", prNumber: 70, installationId: 123 }); + + expect(seen.merged).toBe(true); // no false self-collision from counting the old+new rename names as two files + expect(seen.labels).not.toContain("migration-collision"); + }); + + it("REGRESSION: renaming an EXISTING base migration (same number) does NOT self-collide with its own old name still live on main", async () => { + // Before the fix, liveFilenames (fetched from main, which still has the pre-rename name until this PR + // merges) was unioned as-is with prMigrationFilenames (the new name only) — so a same-number typo-fix + // rename of an ALREADY-MERGED base migration counted as two distinct files at one number and + // self-collided, even though the merged tree would only ever contain the renamed file. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 73, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes("/git/trees/main")) { + seen.treeCalls += 1; + // main still has the PRE-rename name — this PR's rename hasn't merged yet. + return Response.json({ tree: [{ type: "blob", path: "migrations/0099_old.sql" }] }); + } + if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/73/")) { + return Response.json({ number: 73, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); + } + // Renames an EXISTING base migration (same number 0099), not a file this PR itself added. + if (url.includes("/pulls/73/files")) return Response.json([{ filename: "migrations/0099_new.sql", previous_filename: "migrations/0099_old.sql", status: "renamed", additions: 1, deletions: 1, changes: 2, patch: "@@\n rename" }]); + if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); + if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/branches/")) return Response.json({ contexts: [] }); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/pulls/73/merge") && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true, sha: "merged-sha1" }); + } + if (url.includes("/issues/73/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/73/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes("/issues/73/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-rename-existing-base", repoFullName: "owner/repo", prNumber: 73, installationId: 123 }); + + expect(seen.merged).toBe(true); // the pre-rename name still live on main must not count against this PR + expect(seen.labels).not.toContain("migration-collision"); + }); + + it("REGRESSION: renumbering (renaming) this PR's migration to resolve a real collision does not leave a stale hold from the old filename", async () => { + // Before the fix, the stale previousFilename (the OLD number) stayed in prMigrationFilenames forever, + // colliding with an unrelated already-merged file at that old number and permanently re-holding a PR + // that had already fixed itself — exactly the remediation this feature's own comment recommends. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 71, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes("/git/trees/main")) { + seen.treeCalls += 1; + // main already has an unrelated, already-merged file at the OLD number (0099) — nothing to do with + // this PR anymore, since it renumbered away from 0099 to 0100. + return Response.json({ tree: [{ type: "blob", path: "migrations/0099_other_already_merged.sql" }] }); + } + if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/71/")) { + return Response.json({ number: 71, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); + } + if (url.includes("/pulls/71/files")) return Response.json([{ filename: "migrations/0100_mine.sql", previous_filename: "migrations/0099_mine.sql", status: "renamed", additions: 1, deletions: 1, changes: 2, patch: "@@\n rename" }]); + if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); + if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/branches/")) return Response.json({ contexts: [] }); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/pulls/71/merge") && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true, sha: "merged-sha1" }); + } + if (url.includes("/issues/71/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/71/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes("/issues/71/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-renumber-remediation", repoFullName: "owner/repo", prNumber: 71, installationId: 123 }); + + expect(seen.merged).toBe(true); // the stale old-number previousFilename must not re-trigger a hold + expect(seen.labels).not.toContain("migration-collision"); + }); + + it("REGRESSION: deleting this PR's own colliding migration file does not still count it as one of the PR's own filenames", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedMigrationRecheckRepo(env, 72, { premergeContentRecheck: true }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes("/git/trees/main")) { + seen.treeCalls += 1; + return Response.json({ tree: [{ type: "blob", path: "migrations/0099_other_already_merged.sql" }] }); + } + if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/72/")) { + return Response.json({ number: 72, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); + } + // The PR deletes its own migration file (status="removed") — it no longer exists in the PR's tree. + if (url.includes("/pulls/72/files")) return Response.json([{ filename: "migrations/0099_mine.sql", status: "removed", additions: 0, deletions: 5, changes: 5, patch: "@@\n-removed" }]); + if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); + if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); + if (url.includes("/branches/")) return Response.json({ contexts: [] }); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/pulls/72/merge") && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true, sha: "merged-sha1" }); + } + if (url.includes("/issues/72/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/72/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes("/issues/72/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-removed-file", repoFullName: "owner/repo", prNumber: 72, installationId: 123 }); + + // With no migrations/**-touching file left in the PR's own set (the only entry is `status: "removed"`), + // prMigrationFilenames is empty — the whole recheck is path-gated off, so it never even fetches the tree. + expect(seen.treeCalls).toBe(0); + expect(seen.merged).toBe(true); + expect(seen.labels).not.toContain("migration-collision"); + }); + }); + + describe("unlinked-issue guardrail (#unlinked-issue-guardrail, credibility-gate-farming defense)", () => { + // Mirrors the #2550 migration-recheck fixture immediately above: full merge-eligible stub set + // (clean + green + approved) so a positive test proves the hold actually suppresses what would + // otherwise merge, and a negative test proves the guardrail correctly stays out of the way / off by + // default. `run` (the env.AI.run spy) is asserted directly rather than inferred from side effects. + function stubUnlinkedIssueGuardrailFetch(prNumber: number, seen: { closed: boolean; merged: boolean; labels: string[]; comments: string[] }) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); + if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes(`/pulls/${prNumber}/`)) { + return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); + } + // src/github/webhook.ts (not src/queue/**): this block tests the unlinked-issue guardrail specifically, + // and src/queue/** is one of ENGINE_DECISION_GUARDRAIL_GLOBS' built-in invariants (guardrail-config.ts) — + // a diff touching it would unconditionally hold regardless of this guardrail's own on/off setting. + if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([{ filename: "src/github/webhook.ts", status: "modified", additions: 5, deletions: 0, changes: 5, patch: "@@\n+dedupe retries" }]); + if (url.includes(`/commits/sha1/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes(`/commits/sha1/status`)) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); + if (url.includes(`/commits/sha1/check-suites`)) return Response.json({ check_suites: [] }); + if (url.includes("/branches/")) return Response.json({ contexts: [] }); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes(`/pulls/${prNumber}/merge`) && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true, sha: "merged-sha1" }); + } + if (url.includes(`/pulls/${prNumber}`) && method === "PATCH") { + seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; + return Response.json({ number: prNumber, state: "closed" }); + } + if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { + seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); + return Response.json({ id: 1 }, { status: 201 }); + } + if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + return Response.json({}); + }); + } + + async function seedGuardrailRepo(env: Env, prNumber: number, opts: { guardrailMode?: "hold" | "off"; prBody?: string; autonomy?: Record } = {}) { + await upsertInstallation(env, { + installation: { id: 123, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { contents: "write", pull_requests: "write", issues: "write" }, events: [] }, + }); + await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: opts.autonomy ?? { merge: "auto", review_state_label: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + if (opts.guardrailMode !== undefined) { + await upsertRepoFocusManifest(env, "owner/repo", { settings: { unlinkedIssueGuardrail: { mode: opts.guardrailMode } } }); + } + await upsertIssueFromGitHub(env, "owner/repo", { number: 5, title: "webhook retry duplicate bug report", state: "open", user: { login: "someone" }, labels: [], body: "retries duplicate events under heavy load, needs a dedup key" }); + await upsertPullRequestFromGitHub(env, "owner/repo", { number: prNumber, title: "fix webhook retry duplicate bug", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main" }, labels: [], body: opts.prBody ?? "" }); + } + + it("holds a would-otherwise-merge PR when its diff appears to directly solve an existing open issue it never linked", async () => { + const run = vi.fn(async () => ({ response: JSON.stringify({ matched: true, confidence: 0.9, evidence: "adds the missing dedup key" }) })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai }); + await seedGuardrailRepo(env, 80, { guardrailMode: "hold" }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; + stubUnlinkedIssueGuardrailFetch(80, seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-hold", repoFullName: "owner/repo", prNumber: 80, installationId: 123 }); + + expect(seen.merged).toBe(false); + expect(seen.closed).toBe(false); // held, never closed — this is a hold, not a close + expect(seen.labels).toContain("manual-review"); + expect(seen.comments.some((c) => c.includes("#5"))).toBe(true); + expect(run).toHaveBeenCalled(); + }); + + it("is off by default — never calls the AI even for a PR whose diff clearly overlaps an open issue", async () => { + const run = vi.fn(async () => ({ response: JSON.stringify({ matched: true, confidence: 0.9, evidence: "x" }) })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai }); + await seedGuardrailRepo(env, 81); // guardrailMode left unset — defaults off + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; + stubUnlinkedIssueGuardrailFetch(81, seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-off", repoFullName: "owner/repo", prNumber: 81, installationId: 123 }); + + expect(run).not.toHaveBeenCalled(); + expect(seen.merged).toBe(true); + }); + + it("does not call the AI when the PR already links an issue, even with the guardrail on", async () => { + const run = vi.fn(async () => ({ response: JSON.stringify({ matched: true, confidence: 0.9, evidence: "x" }) })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai }); + await seedGuardrailRepo(env, 82, { guardrailMode: "hold", prBody: "Closes #5" }); + const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; + stubUnlinkedIssueGuardrailFetch(82, seen); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-already-linked", repoFullName: "owner/repo", prNumber: 82, installationId: 123 }); + + expect(run).not.toHaveBeenCalled(); + expect(seen.merged).toBe(true); + }); + + it("escalates to a CLOSE on a second confirmed match by the same contributor (#unlinked-issue-guardrail-followup)", async () => { + const run = vi.fn(async () => ({ response: JSON.stringify({ matched: true, confidence: 0.9, evidence: "adds the missing dedup key" }) })); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai }); + + await seedGuardrailRepo(env, 90, { guardrailMode: "hold" }); + const seenFirst = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; + stubUnlinkedIssueGuardrailFetch(90, seenFirst); + await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-repeat-first", repoFullName: "owner/repo", prNumber: 90, installationId: 123 }); + expect(seenFirst.closed).toBe(false); // first confirmed match: held, not closed + expect(seenFirst.merged).toBe(false); + + // The second PR needs `close` autonomy acting for the escalated disposition to actually execute as a + // close (the first PR's hold path only ever needs `merge`/`review_state_label`). + await seedGuardrailRepo(env, 91, { guardrailMode: "hold", autonomy: { merge: "auto", review_state_label: "auto", close: "auto" } }); + const seenSecond = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; + stubUnlinkedIssueGuardrailFetch(91, seenSecond); + await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-repeat-second", repoFullName: "owner/repo", prNumber: 91, installationId: 123 }); + expect(seenSecond.closed).toBe(true); // same contributor's SECOND confirmed match: closed + expect(seenSecond.merged).toBe(false); + }); + }); + + describe("force-fresh-rebase-before-merge gate (#2552)", () => { + // Full merge-eligible stub set (clean + green + approved), reused across scenarios — mirrors the #2550 + // migration-recheck fixture above. `baseAdvancedAt` stubs the NEW /commits/{baseRef} freshness read; + // `null` simulates an unreadable base commit (404). + function stubFreshRebaseFetch(prNumber: number, opts: { baseAdvancedAt: string | null; mergeableState?: string; headSha?: string }, seen: { merged: boolean; updateBranchCalls: number; baseCommitCalls: number }) { + const headSha = opts.headSha ?? "sha1"; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes(`/pulls/${prNumber}/update-branch`) && method === "PUT") { + seen.updateBranchCalls += 1; + return Response.json({ message: "Updating pull request branch." }, { status: 202 }); + } + if (url.endsWith("/commits/main")) { + seen.baseCommitCalls += 1; + if (opts.baseAdvancedAt === null) return new Response("not found", { status: 404 }); + return Response.json({ commit: { committer: { date: opts.baseAdvancedAt } } }); + } + if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes(`/pulls/${prNumber}/`)) { + return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: headSha }, base: { ref: "main", sha: "base" }, mergeable_state: opts.mergeableState ?? "clean", labels: [] }); + } + if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([{ filename: "src/index.ts", status: "modified", additions: 5, deletions: 1, changes: 6, patch: "@@\n+export const x = 1;" }]); + if (url.includes(`/commits/${headSha}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes(`/commits/${headSha}/status`)) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); + if (url.includes(`/commits/${headSha}/check-suites`)) return Response.json({ check_suites: [] }); + if (url.includes("/branches/")) return Response.json({ contexts: [] }); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes(`/pulls/${prNumber}/merge`) && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true, sha: "merged-sha1" }); + } + if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") return Response.json([]); + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + return Response.json({}); + }); + } + + async function seedFreshRebaseRepo(env: Env, prNumber: number, opts: { requireFreshRebaseWindowMinutes?: number | null; autonomy?: Record } = {}) { + await upsertInstallation(env, { + installation: { id: 123, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { contents: "write", pull_requests: "write", issues: "write" }, events: [] }, + }); + await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "owner/repo", + autonomy: opts.autonomy ?? { merge: "auto", update_branch: "auto", label: "auto" }, + autoMaintain: { requireApprovals: 0, mergeMethod: "squash" }, + aiReviewMode: "off", + gatePack: "oss-anti-slop", + gateCheckMode: "enabled", reviewCheckMode: "required", + checkRunMode: "off", + commentMode: "off", + publicSurface: "off", + ...(opts.requireFreshRebaseWindowMinutes !== undefined ? { requireFreshRebaseWindowMinutes: opts.requireFreshRebaseWindowMinutes } : {}), + }); + await upsertPullRequestFromGitHub(env, "owner/repo", { number: prNumber, title: "Fresh rebase PR", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main" }, labels: [], body: "" }); + } + + it("merges normally when the base has not advanced recently", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedFreshRebaseRepo(env, 90, { requireFreshRebaseWindowMinutes: 10 }); + const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; + stubFreshRebaseFetch(90, { baseAdvancedAt: new Date(Date.now() - 60 * 60_000).toISOString() }, seen); // 1h ago, outside a 10m window + + await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-old-base", repoFullName: "owner/repo", prNumber: 90, installationId: 123 }); + + expect(seen.baseCommitCalls).toBe(1); + expect(seen.updateBranchCalls).toBe(0); + expect(seen.merged).toBe(true); + const merge = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.merge").first<{ outcome: string }>(); + expect(merge?.outcome).toBe("completed"); + }); + + it("forces update_branch instead of merging when the base advanced within the freshness window", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedFreshRebaseRepo(env, 91, { requireFreshRebaseWindowMinutes: 10 }); + const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; + stubFreshRebaseFetch(91, { baseAdvancedAt: new Date(Date.now() - 60_000).toISOString() }, seen); // 1 minute ago, within a 10m window + + await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-forced", repoFullName: "owner/repo", prNumber: 91, installationId: 123 }); + + expect(seen.updateBranchCalls).toBe(1); + expect(seen.merged).toBe(false); + const ub = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.update_branch").first<{ outcome: string }>(); + expect(ub?.outcome).toBe("completed"); + const forced = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.forced_rebase_freshness").first<{ outcome: string }>(); + expect(forced?.outcome).toBe("completed"); + const merge = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.merge").first<{ n: number }>(); + expect(merge?.n).toBe(0); + }); + + it("never fetches the base commit or forces a rebase when the setting is unset (off by default)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedFreshRebaseRepo(env, 92); // requireFreshRebaseWindowMinutes left unset + const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; + stubFreshRebaseFetch(92, { baseAdvancedAt: new Date().toISOString() }, seen); // "now" — would force if the setting were on + + await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-off", repoFullName: "owner/repo", prNumber: 92, installationId: 123 }); + + expect(seen.baseCommitCalls).toBe(0); + expect(seen.updateBranchCalls).toBe(0); + expect(seen.merged).toBe(true); + }); + + it("falls through to a normal merge once the bounded-retry cap is reached, with a cap-exceeded audit event", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedFreshRebaseRepo(env, 93, { requireFreshRebaseWindowMinutes: 10 }); + // Seed the bounded-retry counter at the cap (3) for this repo+PR, matching what 3 prior forced + // attempts would have left behind. + await env.SELFHOST_TRANSIENT_CACHE?.set("fresh-rebase-forced:owner/repo#93", "3", 24 * 3600); + const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; + stubFreshRebaseFetch(93, { baseAdvancedAt: new Date(Date.now() - 60_000).toISOString() }, seen); // still within window + + await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-capped", repoFullName: "owner/repo", prNumber: 93, installationId: 123 }); + + expect(seen.updateBranchCalls).toBe(0); // capped — never forces a 4th attempt + expect(seen.merged).toBe(true); // falls through to a normal merge instead + const capped = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.fresh_rebase_window_cap_exceeded").first<{ outcome: string }>(); + expect(capped?.outcome).toBe("completed"); + }); + + it("fails open (merges normally) when the base commit is unreadable", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedFreshRebaseRepo(env, 94, { requireFreshRebaseWindowMinutes: 10 }); + const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; + stubFreshRebaseFetch(94, { baseAdvancedAt: null }, seen); // 404 on the base commit fetch + + await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-unreadable", repoFullName: "owner/repo", prNumber: 94, installationId: 123 }); + + expect(seen.baseCommitCalls).toBe(1); + expect(seen.updateBranchCalls).toBe(0); + expect(seen.merged).toBe(true); + }); + + it("falls through to a normal merge when the forced update_branch action itself is not authorized", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + // update_branch is deliberately absent from autonomy (resolves to the deny-by-default "observe" level), + // while merge stays "auto" — proving the freshness gate fails open independently of the eventual merge + // action's own authorization. + await seedFreshRebaseRepo(env, 95, { requireFreshRebaseWindowMinutes: 10, autonomy: { merge: "auto", label: "auto" } }); + const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; + stubFreshRebaseFetch(95, { baseAdvancedAt: new Date(Date.now() - 60_000).toISOString() }, seen); // within window + + await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-not-authorized", repoFullName: "owner/repo", prNumber: 95, installationId: 123 }); + + expect(seen.baseCommitCalls).toBe(1); + expect(seen.updateBranchCalls).toBe(0); // denied by autonomy before any GitHub mutation is attempted + expect(seen.merged).toBe(true); // falls through to the normal merge decision + const denied = await env.DB.prepare("select outcome from audit_events where event_type = ? order by created_at desc limit 1").bind("agent.action.update_branch").first<{ outcome: string }>(); + expect(denied?.outcome).toBe("denied"); + }); + + it("REGRESSION (gate finding): the bounded-retry counter accumulates across successful forces even though each one changes the head SHA", async () => { + // A successful update_branch itself produces a NEW head SHA (the merge-base-into-head commit). The + // counter must NOT reset just because ITS OWN action changed the head -- otherwise the cap could never + // be reached via the exact path it exists to bound, and a fast-moving base would force a rebase on + // EVERY pass forever. Simulates 3 rounds, each with a genuinely different head SHA (mirroring the + // synchronize webhook a real update_branch triggers), then a 4th round proving the cap holds. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedFreshRebaseRepo(env, 96, { requireFreshRebaseWindowMinutes: 10 }); + const shas = ["sha-r1", "sha-r2", "sha-r3", "sha-r4"]; + + for (const [round, sha] of shas.entries()) { + await upsertPullRequestFromGitHub(env, "owner/repo", { number: 96, title: "Fresh rebase PR", state: "open", user: { login: "contributor" }, head: { sha }, base: { ref: "main" }, labels: [], body: "" }); + const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; + stubFreshRebaseFetch(96, { baseAdvancedAt: new Date(Date.now() - 60_000).toISOString(), headSha: sha }, seen); // always within window + + await processJob(env, { type: "agent-regate-pr", deliveryId: `fresh-rebase-multi-round-${round}`, repoFullName: "owner/repo", prNumber: 96, installationId: 123 }); + + if (round < 3) { + // Rounds 0-2 (attempts 1-3): still under/at the cap -- forces update_branch, never merges. + expect(seen.updateBranchCalls).toBe(1); + expect(seen.merged).toBe(false); + } else { + // Round 3 (the 4th evaluation): the cap (3) was already reached by round 2 -- falls through to merge. + expect(seen.updateBranchCalls).toBe(0); + expect(seen.merged).toBe(true); + } + } + + const forcedCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.forced_rebase_freshness").first<{ n: number }>(); + expect(forcedCount?.n).toBe(3); // exactly 3 successful forces, not 4 + const capped = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.fresh_rebase_window_cap_exceeded").first<{ outcome: string }>(); + expect(capped?.outcome).toBe("completed"); + }); + }); + + it("contributor open-PR cap (#2270): a contributor's 3rd open PR (over a cap of 2) is labeled + closed deterministically with no merit merge", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "n/a", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // Two PRE-EXISTING open PRs from the same author, seeded directly (as if opened moments earlier). + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + // The label is the configurable `.gittensory.yml` value below — nothing is hard-coded. + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorCapLabel: "spam-cap" } }, "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(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/55/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-close", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + // Deterministic gate: closed + labeled (with the configured label), and the AI was NEVER called. + expect(aiCalls).toBe(0); + expect(seen.closed).toBe(true); + expect(seen.labels).toContain("spam-cap"); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + // No merit merge despite a clean+green+approved PR (the cap short-circuits ahead of merit). + const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); + expect(mergeAudit?.n).toBe(0); + // The close comment states the cap + current count (public, unlike the blacklist's static-only comment). + expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open pull requests") && c.includes("limit of 2"))).toBe(true); + }); + + it("contributor open-PR cap (#2270): a maintainer-named autoCloseExemptLogins entry is exempt from the PER-REPO cap too (not just the install-wide cap)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // Two PRE-EXISTING open PRs from an exempt bot author (e.g. a third-party automation App like Sentry's Seer + // fix bot) — same over-cap shape as the "3rd PR" test above, but this login is on autoCloseExemptLogins. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Sentry fix one", state: "open", user: { login: "sentry[bot]" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Sentry fix two", state: "open", user: { login: "sentry[bot]" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + 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(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "sentry[bot]" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/55/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-exempt-login", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Sentry's 3rd PR", state: "open", user: { login: "sentry[bot]" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + // Exempt: the 3rd PR is NOT closed or labeled for the cap, despite being (numerically) over it. + expect(seen.closed).toBe(false); + expect(seen.labels).not.toContain("over-contributor-limit"); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBe(0); + }); + + function stubContributorCapCiCancelFetch(seen: { closed: boolean; cancelledIds: number[]; listedStatuses: string[] }, runListResponses: { in_progress?: number[]; queued?: number[] } = {}, cancelResponse: () => Response = () => new Response(null, { status: 202 })) { + return async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + // The other-siblings live-state recheck (#2270 complete-set fix) confirms every counted sibling PR is + // still open before trusting it toward the cap — farmer99's two pre-existing PRs (53, 54) must report open. + if (url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) return Response.json({ number: 53, state: "open" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels")) return Response.json([]); + if (url.includes("/issues/55/comments")) return Response.json([]); + if (url.includes("/actions/runs?head_sha=f55&status=in_progress")) { seen.listedStatuses.push("in_progress"); return Response.json({ workflow_runs: (runListResponses.in_progress ?? []).map((id) => ({ id, event: "pull_request", pull_requests: [{ number: 55 }] })) }); } + if (url.includes("/actions/runs?head_sha=f55&status=queued")) { seen.listedStatuses.push("queued"); return Response.json({ workflow_runs: (runListResponses.queued ?? []).map((id) => ({ id, event: "pull_request", pull_requests: [{ number: 55 }] })) }); } + if (url.includes("/actions/runs/") && url.endsWith("/cancel") && method === "POST") { + seen.cancelledIds.push(Number(url.match(/\/actions\/runs\/(\d+)\/cancel/)?.[1])); + return cancelResponse(); + } + return Response.json({}); + }; + } + + it("contributor open-PR cap (#2462): a contributor_cap close cancels the PR's in-flight CI runs when contributorCapCancelCi is enabled", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + contributorCapCancelCi: true, + }); + const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; + vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [101], queued: [102] })); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-cancel-ci-enabled", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.listedStatuses.sort()).toEqual(["in_progress", "queued"]); + expect(seen.cancelledIds.sort()).toEqual([101, 102]); + const cancelAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.contributor_cap_ci_cancelled'").first<{ n: number }>(); + expect(cancelAudit?.n).toBeGreaterThanOrEqual(1); + }); + + it("contributor open-PR cap (#2462): a failing cancel-success audit write does not throw — the close still completes", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + 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; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.contributor_cap_ci_cancelled") throw new Error("audit DB down"); + await originalRecordAuditEvent(auditEnv, event); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-cancel-ci-audit-fail", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }), + ).resolves.toBeUndefined(); + auditSpy.mockRestore(); + expect(seen.closed).toBe(true); + }); + + it("contributor open-PR cap (#2462): a failing cancel-FAILURE audit write also does not throw — the close still completes", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + contributorCapCancelCi: true, + }); + const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; + vi.stubGlobal( + "fetch", + stubContributorCapCiCancelFetch(seen, { in_progress: [104] }, () => new Response(null, { status: 500 })), + ); + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.contributor_cap_ci_cancel_failed") throw new Error("audit DB down"); + await originalRecordAuditEvent(auditEnv, event); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-cancel-ci-failed-audit-fail", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }), + ).resolves.toBeUndefined(); + auditSpy.mockRestore(); + expect(seen.closed).toBe(true); + }); + + it("contributor open-PR cap (#2462): contributorCapCancelCi unset (default) never attempts to cancel CI runs", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + // contributorCapCancelCi intentionally omitted — off by default, no CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT set. + }); + const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; + vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [201] })); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-cancel-ci-off", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.listedStatuses).toEqual([]); + expect(seen.cancelledIds).toEqual([]); + }); + + it("contributor open-PR cap (#2462): a missing actions:write permission degrades gracefully — the close still succeeds and a permission_missing audit is recorded", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + 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(); + const method = init?.method ?? "GET"; + if (url.includes("/actions/runs?head_sha=")) return Response.json({ message: "Resource not accessible by integration" }, { status: 403 }); + return stubContributorCapCiCancelFetch(seen)(input, init); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-cancel-ci-permission-missing", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + // The close itself still succeeded and is recorded "completed", NOT "error" -- the cancel-permission gap + // must never retroactively fail an already-successful close (#2462 core requirement). + expect(seen.closed).toBe(true); + const closeAudit = await env.DB.prepare("select outcome from audit_events where event_type = 'agent.action.close' order by created_at desc limit 1").first<{ outcome: string }>(); + expect(closeAudit?.outcome).toBe("completed"); + const permissionAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.contributor_cap_ci_cancel_permission_missing'").first<{ n: number }>(); + expect(permissionAudit?.n).toBeGreaterThanOrEqual(1); + }); + + it("contributor open-PR cap (#2462, #gate finding): a genuine cancel error (not a permission gap) is recorded under its own event type, distinct from permission_missing", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + contributorCapCancelCi: true, + }); + const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; + vi.stubGlobal( + "fetch", + stubContributorCapCiCancelFetch(seen, { in_progress: [901] }, () => new Response(null, { status: 500 })), + ); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-cancel-ci-generic-error", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(true); // the close itself still succeeds regardless of the cancel outcome + const failedAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.contributor_cap_ci_cancel_failed'").first<{ n: number }>(); + expect(failedAudit?.n).toBeGreaterThanOrEqual(1); + const permissionAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.contributor_cap_ci_cancel_permission_missing'").first<{ n: number }>(); + expect(permissionAudit?.n).toBe(0); // a generic 500 must never be misclassified as a permission gap + }); + + it("contributor open-PR cap (#2462): CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT env var enables cancellation when the repo hasn't configured its own value", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT: "true" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + // contributorCapCancelCi intentionally omitted (null) -- falls back to the env var default above. + }); + const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; + vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [301] })); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-cancel-ci-env-default", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.cancelledIds).toEqual([301]); + }); + + it("contributor open-PR cap (#2462): an explicit repo-level contributorCapCancelCi: false overrides a true CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT: "true" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + contributorCapCancelCi: false, + }); + const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; + vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [401] })); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-cancel-ci-repo-override", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.listedStatuses).toEqual([]); + expect(seen.cancelledIds).toEqual([]); + }); + + it("contributor open-PR cap (#2270): uses a complete author-scoped set beyond the duplicate-analysis 100-row sample (regression)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + for (let number = 1; number <= 100; number += 1) { + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number, title: `Busy repo PR ${number}`, state: "open", user: { login: `other-${number}` }, head: { sha: `o${number}` }, labels: [], body: "x" }); + } + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 101, title: "Spammer PR one", state: "open", user: { login: "spammer" }, head: { sha: "s101" }, labels: [], body: "x" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 1, + }); + const seen = { closed: false, comments: [] as string[] }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/pulls/101") && method === "GET") return Response.json({ number: 101, state: "open" }); + if (url.includes("/pulls/102/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/102/reviews")) return Response.json([]); + if (url.includes("/pulls/102/commits")) return Response.json([]); + if (url.endsWith("/pulls/102") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 102, state: "closed" }); } + if (url.endsWith("/pulls/102")) return Response.json({ number: 102, state: "open", user: { login: "spammer" }, head: { sha: "s102" }, mergeable_state: "clean" }); + if (url.includes("/commits/s102/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/s102/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/102/labels")) return Response.json([]); + if (url.includes("/issues/102/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/issues/102/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-busy-repo", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 102, title: "Spammer PR two", state: "open", user: { login: "spammer" }, head: { sha: "s102" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.comments.some((c) => c.includes("@spammer") && c.includes("2 open pull requests") && c.includes("limit of 1"))).toBe(true); + }); + + it("REGRESSION (security review finding): the per-repo cap's sibling live-check bounds concurrency instead of firing one request per open PR at once", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // 30 OTHER open PRs from the SAME author — well beyond CONTRIBUTOR_CAP_LIVE_CHECK_CONCURRENCY (10), so an + // unbounded Promise.all would fire all 30 live-state GETs at once. + const SIBLING_COUNT = 30; + for (let number = 1; number <= SIBLING_COUNT; number += 1) { + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number, title: `Prolific PR ${number}`, state: "open", user: { login: "prolific" }, head: { sha: `p${number}` }, labels: [], body: "x" }); + } + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 100, // above SIBLING_COUNT + 1 — this test only cares about concurrency, not closing. + }); + let inFlight = 0; + let maxInFlight = 0; + const siblingCheckPattern = new RegExp(`/pulls/(?:${Array.from({ length: SIBLING_COUNT }, (_, i) => i + 1).join("|")})$`); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (siblingCheckPattern.test(url) && method === "GET") { + inFlight += 1; + maxInFlight = Math.max(maxInFlight, inFlight); + // A tiny real delay forces genuine overlap between concurrently-dispatched sibling checks — without + // it, each mock resolves synchronously and never actually overlaps another in-flight call. + await new Promise((resolve) => setTimeout(resolve, 5)); + inFlight -= 1; + return Response.json({ state: "open" }); + } + if (url.includes(`/pulls/${SIBLING_COUNT + 1}/files`)) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes(`/pulls/${SIBLING_COUNT + 1}/reviews`)) return Response.json([]); + if (url.includes(`/pulls/${SIBLING_COUNT + 1}/commits`)) return Response.json([]); + if (url.endsWith(`/pulls/${SIBLING_COUNT + 1}`)) return Response.json({ number: SIBLING_COUNT + 1, state: "open", user: { login: "prolific" }, head: { sha: `p${SIBLING_COUNT + 1}` }, mergeable_state: "clean" }); + if (url.includes(`/commits/p${SIBLING_COUNT + 1}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes(`/commits/p${SIBLING_COUNT + 1}/status`)) return Response.json({ state: "success", statuses: [] }); + if (url.includes(`/issues/${SIBLING_COUNT + 1}/labels`)) return Response.json([]); + if (url.includes(`/issues/${SIBLING_COUNT + 1}/comments`)) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-bounded-concurrency", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: SIBLING_COUNT + 1, title: "Prolific author's newest PR", state: "open", user: { login: "prolific" }, head: { sha: `p${SIBLING_COUNT + 1}` }, labels: [], body: "x", mergeable_state: "clean" }, + }, + }); + + expect(maxInFlight).toBeGreaterThan(1); // proves the check is genuinely concurrent, not accidentally serial + expect(maxInFlight).toBeLessThanOrEqual(10); // CONTRIBUTOR_CAP_LIVE_CHECK_CONCURRENCY + }); + + it("contributor open-PR cap (#2270): disabled (no cap configured, the default) never closes an over-threshold contributor", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + // No contributorOpenPrCap set — the default, disabled state. + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-disabled", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + expect(seen.closed).toBe(false); + }); + + it("contributor open-PR cap (#2270): a contributor's 2nd PR AT (not over) a cap of 2 is not closed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // Only ONE pre-existing open PR from this author — the incoming PR is their 2nd, exactly at the cap. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-at-limit", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 2nd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + expect(seen.closed).toBe(false); + }); + + it("install-wide contributor open-item cap (#2562): an actor over the install-wide cap but under EVERY individual repo's own cap is still caught", async () => { + // No per-repo contributorOpenPrCap is configured on EITHER repo -- only the install-wide env cap. One + // pre-existing open PR on repo-a and one on repo-b (2 total), plus the incoming 3rd (also on repo-a) = 3, + // over a global cap of 2 -- even though repo-a's own count (2) and repo-b's own count (1) would each + // individually be unremarkable (and no per-repo cap is even configured to catch them). + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + // upsertInstallation's own `repositories:` array is NOT itself persisted to the `repositories` table (only + // the `installations` row) -- the real webhook pipeline registers a repo's installationId as a side effect + // of processing an event FOR that repo, which never happens here for repo-b (the non-webhook-triggered repo). + // Register it explicitly so countOpenItemsForAuthorAcrossRepos's installation-scoped lookup can find its rows. + await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-a", { number: 20, title: "Farmer PR on repo-a", state: "open", user: { login: "farmer99" }, head: { sha: "fa20" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/repo-a", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + // Deliberately NO contributorOpenPrCap here — only the install-wide env cap should catch this. + }); + const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + // Install-wide live-verify (#2562 gate-review follow-up) re-fetches every OTHER counted sibling before + // trusting it toward the cap -- both of farmer99's other open items must resolve as confirmed-open here. + if (url.endsWith("/repos/JSONbored/repo-a/pulls/20")) return Response.json({ number: 20, state: "open" }); + if (url.endsWith("/repos/JSONbored/repo-b/pulls/10")) return Response.json({ number: 10, state: "open" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/55/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-cap-close", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.labels).toContain("over-contributor-limit"); + // Install-wide cap counts BOTH open PRs and open issues together (#2562 gate-review follow-up), so the + // close message reports the mixed noun rather than a stale "pull requests"-only phrasing. + expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open pull requests and issues") && c.includes("across every repository it gates"))).toBe(true); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + }); + + it("install-wide contributor open-item cap (#2562): stops live verification after the cap is exceeded", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "1" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }], + }); + for (let number = 1; number <= 30; number += 1) { + await upsertPullRequestFromGitHub(env, "JSONbored/repo-a", { number, title: `Farmer PR ${number}`, state: "open", user: { login: "farmer99" }, head: { sha: `fa${number}` }, labels: [], body: "x" }); + } + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/repo-a", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + }); + const seen = { closed: false, livePullReads: [] as number[] }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + const siblingPull = url.match(/\/repos\/JSONbored\/repo-a\/pulls\/(\d+)$/); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (siblingPull && siblingPull[1] !== "55") { seen.livePullReads.push(Number(siblingPull[1])); return Response.json({ number: Number(siblingPull[1]), state: "open" }); } + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-cap-short-circuit", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 31st PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.livePullReads).toHaveLength(10); + expect(seen.livePullReads).not.toContain(11); + }); + + it("install-wide contributor open-item cap (#2562, #4511): env var unset falls back to the real default (20), so a spread-across-repos actor well under it is not closed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // no GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP -- resolves to the DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP, not "no cap" (#4511) + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/repo-a", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-cap-off-by-default", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + expect(seen.closed).toBe(false); + }); + + it("install-wide contributor open-item cap (#2562): a maintainer-named autoCloseExemptLogins entry is exempt from the install-wide cap", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/repo-a", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + autoCloseExemptLogins: ["farmer99"], + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-cap-exempt", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + expect(seen.closed).toBe(false); + }); + + it("install-wide contributor open-item cap (#2562): an author AT (not over) the configured install-wide cap is not closed", async () => { + // Global cap is configured (2) and reached exactly (repo-b's 1 pre-existing + this incoming PR = 2), so the + // install-wide check must fall through without matching -- the `installOpenCount > globalCap` false branch. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/repo-a", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-cap-at-limit", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 2nd PR, at the install-wide limit", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + expect(seen.closed).toBe(false); + }); + + it("install-wide contributor open-item cap (#4511): a CONFIRMED official Gittensor miner gets the higher miner-specific cap, not the human one, even though the human cap alone would already be exceeded", async () => { + // Human cap (2) would already be exceeded by 3 open items -- but farmer99 resolves as a confirmed miner via + // the /miners API, so the fleet-appropriate default (50, GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER unset) applies + // instead, and 3 is nowhere near that. Must fall through without matching. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); + await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 11, title: "Farmer 2nd PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb11" }, labels: [], body: "z" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/repo-a", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "farmer99", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://api.gittensor.io/miners/123") return Response.json({}); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/10") || url.endsWith("/pulls/11")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-cap-confirmed-miner", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Confirmed miner's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + expect(seen.closed).toBe(false); + }); + + it("contributor open-PR cap (#2270): the repo OWNER's own PR is never closed even over the cap (live processor path, not just the planner)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Owner PR one", state: "open", user: { login: "JSONbored" }, head: { sha: "o53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Owner PR two", state: "open", user: { login: "JSONbored" }, head: { sha: "o54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "JSONbored" }, head: { sha: "o55" }, mergeable_state: "clean" }); + if (url.includes("/commits/o55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/o55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-owner", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Owner's 3rd PR", state: "open", user: { login: "JSONbored" }, head: { sha: "o55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(seen.closed).toBe(false); + }); + + it("contributor open-PR cap (#2270): an author-less (ghost) open PR among the repo's others is excluded from the count and the sibling-wake scan, not crashed on", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // A ghost PR with no `user` at all (authorLogin ends up null) — must not match farmer99's count, and must + // not crash the sibling-wake scan, which runs the identical (authorLogin ?? "") fallback. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 50, title: "Ghost PR", state: "open", head: { sha: "ghost50" }, labels: [], body: "z" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-cap-ghost-author", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + // Ghost PR's null authorLogin never matches "farmer99" — the count is still exactly 3 (farmer99's own). + expect(seen.closed).toBe(true); + }); + + function stubAccountAgeFetch(prNumber: number, createdAt: string, seen: { labels: string[]; closed: boolean }) { + return async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) return Response.json({ login: "newbie", created_at: createdAt }); + if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes(`/pulls/${prNumber}/reviews`)) return Response.json([]); + if (url.includes(`/pulls/${prNumber}/commits`)) return Response.json([]); + if (url.endsWith(`/pulls/${prNumber}`) && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: prNumber, state: "closed" }); } + if (url.endsWith(`/pulls/${prNumber}`)) return Response.json({ number: prNumber, state: "open", user: { login: "newbie" }, head: { sha: `s${prNumber}` }, mergeable_state: "clean" }); + // The other-siblings live-state recheck (#2270 complete-set fix) confirms every counted sibling PR is + // still open before trusting it toward the cap — a generic catch-all covers any of newbie's other + // pre-existing PR numbers without hard-coding specific ones. + if (/\/pulls\/\d+$/.test(url)) return Response.json({ state: "open" }); + if (url.includes(`/commits/s${prNumber}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes(`/commits/s${prNumber}/status`)) return Response.json({ state: "success", statuses: [] }); + if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); + return Response.json({}); + }; + } + + it("account-age throttle (#2561): a below-threshold-age account gets the new-account label AND a tighter effective cap", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // Two pre-existing open PRs from the same new author — a cap of 4 (tightened to 2 for a new account) + // means the 3rd PR is already over the tightened cap, even though it's well under the CONFIGURED cap of 4. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie PR one", state: "open", user: { login: "newbie" }, head: { sha: "s60" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie PR two", state: "open", user: { login: "newbie" }, head: { sha: "s61" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + gateCheckMode: "enabled", reviewCheckMode: "required", + // #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, + }); + 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)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-tighter-cap", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 62, title: "Newbie's 3rd PR", state: "open", user: { login: "newbie" }, head: { sha: "s62" }, labels: [], body: "x", mergeable_state: "clean" }, + }, + }); + + expect(seen.labels).toContain("new-account"); + // The tightened cap (ceil(4/2)=2) is already exceeded by the 3rd PR — closed despite being under the raw cap of 4. + expect(seen.closed).toBe(true); + }); + + it("account-age throttle (#2561): stale cached sibling PRs do not inflate the tightened cap into an auto-close", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 66, title: "Stale newbie PR", state: "open", user: { login: "newbie" }, head: { sha: "s66" }, labels: [], body: "x" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenPrCap: 2, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) return Response.json({ login: "newbie", created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString() }); + if (url.endsWith("/pulls/66")) return Response.json({ number: 66, state: "closed" }); + if (url.includes("/pulls/67/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/67/reviews")) return Response.json([]); + if (url.includes("/pulls/67/commits")) return Response.json([]); + if (url.endsWith("/pulls/67") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 67, state: "closed" }); } + if (url.endsWith("/pulls/67")) return Response.json({ number: 67, state: "open", user: { login: "newbie" }, head: { sha: "s67" }, mergeable_state: "clean" }); + if (url.includes("/commits/s67/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/s67/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/67/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/67/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + if (url.includes("/issues/67/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-stale-tight-cap", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 67, title: "Newbie's live PR", state: "open", user: { login: "newbie" }, head: { sha: "s67" }, labels: [], body: "x", mergeable_state: "clean" }, + }, + }); + + expect(seen.labels).toContain("new-account"); + expect(seen.closed).toBe(false); + }); + + it("account-age throttle (#2561): an account OLDER than the threshold is unaffected — no label, no cap tightening", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 63, title: "Vet PR one", state: "open", user: { login: "newbie" }, head: { sha: "s63" }, labels: [], body: "x" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 64, title: "Vet PR two", state: "open", user: { login: "newbie" }, head: { sha: "s64" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto", label: "auto" }, + 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)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-unaffected", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 65, title: "Vet's 3rd PR", state: "open", user: { login: "newbie" }, head: { sha: "s65" }, labels: [], body: "x", mergeable_state: "clean" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + // The RAW cap (4) is not yet exceeded by a 3rd PR — untouched. + expect(seen.closed).toBe(false); + }); + + it("account-age throttle (#2561): the repo OWNER's own PR is never labeled even on a brand-new account", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + gateCheckMode: "enabled", reviewCheckMode: "required", + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) return Response.json({ login: "JSONbored", created_at: new Date().toISOString() }); + if (url.includes("/pulls/66/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/66/reviews")) return Response.json([]); + if (url.includes("/pulls/66/commits")) return Response.json([]); + if (url.endsWith("/pulls/66")) return Response.json({ number: 66, state: "open", user: { login: "JSONbored" }, head: { sha: "s66" }, mergeable_state: "clean" }); + if (url.includes("/commits/s66/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/s66/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/66/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/66/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/66/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-owner-exempt", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 66, title: "Owner's own PR", state: "open", user: { login: "JSONbored" }, head: { sha: "s66" }, labels: [], body: "x", mergeable_state: "clean" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + }); + + it("account-age throttle (#2561): disabled (no threshold configured, the default) never fetches the GitHub user or labels", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto", label: "auto" }, + // accountAgeThresholdDays intentionally omitted — off by default. + }); + const seen = { labels: [] as string[], accountAgeUsersFetched: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // Distinct from the UNRELATED, always-on public-contributor-profile lookup (src/github/public.ts), which + // hits this same bare /users/{login} URL but with NO authorization header (GITHUB_PUBLIC_TOKEN unset in + // this test) — only getGithubUserCreatedAt's account-age-specific call sends a Bearer installation token. + if (url.includes("/users/") && (init?.headers as Record | undefined)?.authorization) { + seen.accountAgeUsersFetched = true; + return Response.json({ login: "newbie", created_at: new Date().toISOString() }); + } + if (url.includes("/users/")) return Response.json({ login: "newbie" }); + if (url.includes("/pulls/67/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/67/reviews")) return Response.json([]); + if (url.includes("/pulls/67/commits")) return Response.json([]); + if (url.endsWith("/pulls/67")) return Response.json({ number: 67, state: "open", user: { login: "newbie" }, head: { sha: "s67" }, mergeable_state: "clean" }); + if (url.includes("/commits/s67/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/s67/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/67/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/67/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/67/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-disabled", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 67, title: "Newbie's PR", state: "open", user: { login: "newbie" }, head: { sha: "s67" }, labels: [], body: "x", mergeable_state: "clean" }, + }, + }); + + expect(seen.accountAgeUsersFetched).toBe(false); + expect(seen.labels).not.toContain("new-account"); + }); + + it("account-age throttle (#2561): a configured newAccountLabel is used instead of the default", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto", review_state_label: "auto" }, + 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)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-custom-label", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 68, title: "Newbie's PR", state: "open", user: { login: "newbie" }, head: { sha: "s68" }, labels: [], body: "x", mergeable_state: "clean" }, + }, + }); + + expect(seen.labels).toContain("custom-new-account-label"); + expect(seen.labels).not.toContain("new-account"); + }); + + it("account-age throttle (#2561): a below-threshold account is NOT labeled when the repo has not opted into label autonomy (regression, gate finding)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + gateCheckMode: "enabled", reviewCheckMode: "required", + // autonomy intentionally omitted — deny-by-default ("observe" for every action class, including "review_state_label"). + 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)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-label-not-autonomous", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 69, title: "Newbie's PR", state: "open", user: { login: "newbie" }, head: { sha: "s69" }, labels: [], body: "x", mergeable_state: "clean" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + }); + + function stubIssueAccountAgeFetch(issueNumber: number, createdAt: string, seen: { labels: string[]; closed: boolean }) { + return async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) return Response.json({ login: "newbie", created_at: createdAt }); + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith(`/issues/${issueNumber}`) && method === "PATCH") { + seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; + return Response.json({ state: "closed" }); + } + if (url.includes(`/issues/${issueNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${issueNumber}/labels`) && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.includes(`/issues/${issueNumber}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); + return Response.json({}); + }; + } + + it("account-age throttle (#2561 issue path): a below-threshold-age account gets the new-account label AND a tighter effective issue cap", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenIssueCap: 4, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-tighter-cap", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).toContain("new-account"); + expect(seen.closed).toBe(true); + }); + + it("account-age throttle (#2561 issue path): when accountAgeThresholdDays is off, no user lookup runs", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenIssueCap: 4, + }); + let accountAgeUsersFetched = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) { accountAgeUsersFetched = true; return Response.json({ login: "newbie", created_at: new Date().toISOString() }); } + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") return Response.json({ state: "open" }); + if (url.includes("/issues/62/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-off", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(accountAgeUsersFetched).toBe(false); + }); + + it("account-age throttle (#2561 issue path): established account uses the full issue cap (no tightening)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Oldbie issue one", state: "open", user: { login: "oldbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Oldbie issue two", state: "open", user: { login: "oldbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenIssueCap: 4, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 730 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-established", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Oldbie's 3rd issue", state: "open", user: { login: "oldbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + expect(seen.closed).toBe(false); + }); + + it("account-age throttle (#2561 issue path): does not label when review_state_label is not auto", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto" }, + contributorOpenIssueCap: 4, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-label-not-autonomous", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + expect(seen.closed).toBe(true); + }); + + it("account-age throttle (#2561 issue path): user lookup failure fail-opens to the full configured cap", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + contributorOpenIssueCap: 4, + accountAgeThresholdDays: 30, + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) return new Response("not found", { status: 404 }); + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } + if (url.includes("/issues/62/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-lookup-fail-open", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + }); + + it("account-age throttle (#2561 issue path): a configured newAccountLabel is used instead of the default", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + newAccountLabel: "custom-new-account-label", + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-custom-label", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Newbie's issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).toContain("custom-new-account-label"); + expect(seen.labels).not.toContain("new-account"); + }); + + it("account-age throttle (#2561 issue path): the repo OWNER's own issue is never labeled even on a brand-new account", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) return Response.json({ login: "JSONbored", created_at: new Date().toISOString() }); + if (url.includes("/issues/70/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/70/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-owner-exempt", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 70, title: "Owner's own issue", state: "open", user: { login: "JSONbored" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + }); + + it("account-age throttle (#2561 issue path): an ADMIN_GITHUB_LOGINS author is never labeled even on a brand-new account", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + ADMIN_GITHUB_LOGINS: "fleet-admin", + }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) return Response.json({ login: "fleet-admin", created_at: new Date().toISOString() }); + if (url.includes("/issues/71/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/71/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-admin-exempt", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 71, title: "Admin's issue", state: "open", user: { login: "fleet-admin" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + }); + + it("account-age throttle (#2561 issue path): a protected automation bot author is never labeled even on a brand-new account", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", review_state_label: "auto" }, + accountAgeThresholdDays: 30, + }); + const seen = { labels: [] as string[], closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/users/")) return Response.json({ login: "dependabot[bot]", created_at: new Date().toISOString() }); + if (url.includes("/issues/72/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/72/labels") && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "account-age-issue-bot-exempt", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 72, title: "Bot issue", state: "open", user: { login: "dependabot[bot]" }, labels: [], body: "x" }, + }, + }); + + expect(seen.labels).not.toContain("new-account"); + }); + + it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { + // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ + // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), + // so it correctly stays open — but a naive "only ever check myself" implementation would leave it open + // FOREVER, since nothing else ever re-evaluates PR56 again. This pins the fix: once PR55's delivery later + // sees the COMPLETE set {54, 55, 56}, it must wake PR56 (not just decide for itself) so PR56 gets a fresh, + // fully-gated re-evaluation and self-corrects. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR zero", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "w" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + }); + const closedNumbers = new Set(); + 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) => { + if (message.type === "agent-regate-pr") fanned.push(message); + return realSend(message, options); + }) as typeof env.JOBS.send; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + for (const [n, sha] of [[54, "f54"], [55, "f55"], [56, "f56"]] as const) { + if (url.includes(`/pulls/${n}/files`)) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes(`/pulls/${n}/reviews`)) return Response.json([]); + if (url.includes(`/pulls/${n}/commits`)) return Response.json([]); + if (url.endsWith(`/pulls/${n}`) && method === "PATCH") { closedNumbers.add(n); return Response.json({ number: n, state: "closed" }); } + if (url.endsWith(`/pulls/${n}`)) return Response.json({ number: n, state: closedNumbers.has(n) ? "closed" : "open", user: { login: "farmer99" }, head: { sha }, mergeable_state: "clean" }); + if (url.includes(`/commits/${sha}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes(`/commits/${sha}/status`)) return Response.json({ state: "success", statuses: [] }); + if (url.includes(`/issues/${n}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${n}/labels`) && method === "POST") return Response.json([]); + if (url.includes(`/issues/${n}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes(`/issues/${n}/comments`)) return Response.json([]); + } + return Response.json({}); + }); + + // PR56 arrives FIRST — PR55 does not exist yet, so PR56 sees only {54, 56}: at the cap, not over. + await processJob(env, { + type: "github-webhook", + deliveryId: "burst-pr56-first", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 56, title: "Farmer PR two (out of order)", state: "open", user: { login: "farmer99" }, head: { sha: "f56" }, labels: [], body: "y", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + expect(closedNumbers.has(56)).toBe(false); // correctly not closed YET — the set looked complete at the time + + // PR55 arrives SECOND — now the complete set {54, 55, 56} is visible. PR55 itself ranks within the cap + // (oldest 2 of 3), so it stays open — but PR56 is now discoverably over-cap and must be woken. + await processJob(env, { + type: "github-webhook", + deliveryId: "burst-pr55-second", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + expect(closedNumbers.has(55)).toBe(false); // PR55 itself is within the cap + expect(fanned.some((job) => job.type === "agent-regate-pr" && job.prNumber === 56)).toBe(true); // sibling woken + + // Drain the woken job — PR56's OWN fresh re-evaluation now sees the complete set and self-corrects. + env.JOBS.send = realSend; + for (const job of fanned) await processJob(env, job); + expect(closedNumbers.has(56)).toBe(true); + }); + + it("contributor open-PR cap (#2270): a re-delivered sibling-wake is coalesced — the second discovery does not re-enqueue", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // Pre-seed the coalescing key for PR56 exactly as wakeOverCapSiblingPullRequests itself would after a + // first, already-successful enqueue — proving the SECOND discovery within the window skips re-enqueueing. + await env.SELFHOST_TRANSIENT_CACHE?.set("contributor-cap-wake:jsonbored/gittensory#56", "1", 60); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR zero", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "w" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 56, title: "Farmer PR two (already over cap)", state: "open", user: { login: "farmer99" }, head: { sha: "f56" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + 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) => { + if (message.type === "agent-regate-pr") fanned.push(message); + return realSend(message, options); + }) as typeof env.JOBS.send; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels")) return Response.json([]); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + // PR55 arrives and independently discovers PR56 is over cap — but the wake was already claimed. + await processJob(env, { + type: "github-webhook", + deliveryId: "wake-coalesce-second", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + + expect(fanned).toEqual([]); // coalesced — no duplicate wake enqueued + }); + + it("contributor open-PR cap (#2270): swallows a failed sibling-wake enqueue and does not claim the coalescing key (regression)", async () => { + // If env.JOBS.send() throws (queue backpressure/outage), the wake must be a best-effort fire-and-forget: + // log and move on WITHOUT claiming the coalescing key, so a later discovery can still retry the enqueue. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR zero", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "w" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 56, title: "Farmer PR two (already over cap)", state: "open", user: { login: "farmer99" }, head: { sha: "f56" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "advisory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenPrCap: 2, + }); + env.JOBS.send = (async () => { + throw new Error("queue send boom"); + }) as typeof env.JOBS.send; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); + if (url.includes("/pulls/55/reviews")) return Response.json([]); + if (url.includes("/pulls/55/commits")) return Response.json([]); + if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); + if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/55/labels")) return Response.json([]); + if (url.includes("/issues/55/comments")) return Response.json([]); + return Response.json({}); + }); + + // PR55 arrives, discovers PR56 is over cap, and the wake enqueue itself fails — must not throw. + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "wake-enqueue-fails", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }), + ).resolves.not.toThrow(); + + // The coalescing key was NOT claimed (enqueue failed), so a later discovery can still retry. + expect(await env.SELFHOST_TRANSIENT_CACHE?.get("contributor-cap-wake:jsonbored/gittensory#56")).toBeNull(); + }); + + it("contributor open-ISSUE cap (#2270): a contributor's 3rd open issue (over a cap of 2) is labeled + closed deterministically", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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"); + const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } + if (url.includes("/issues/62/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/62/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/62/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-close", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.labels).toContain("spam-cap"); + expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open issues") && c.includes("limit of 2"))).toBe(true); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + }); + + it("contributor open-ISSUE cap (#2270): bounds the sibling live-check fan-out instead of firing one request per open issue at once (#2766 parity)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // SIBLING_COUNT other open issues from the same author, well beyond the concurrency bound, so an unbounded + // Promise.all would fire every live-state GET at once. The cap is set BELOW the total so the newest issue is + // over the cap and the sibling live-verification path actually runs (it walks the complete sibling set). + const SIBLING_COUNT = 30; + const EXPECTED_LIVE_CHECK_CONCURRENCY = 10; // mirrors CONTRIBUTOR_CAP_LIVE_CHECK_CONCURRENCY in processors.ts + const newIssue = SIBLING_COUNT + 1; + for (let number = 1; number <= SIBLING_COUNT; number += 1) { + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number, title: `Prolific issue ${number}`, state: "open", user: { login: "prolific" }, labels: [], body: "x" }); + } + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenIssueCap: SIBLING_COUNT, + }); + let inFlight = 0; + let maxInFlight = 0; + let closed = false; + const siblingCheckPattern = new RegExp(`/issues/(?:${Array.from({ length: SIBLING_COUNT }, (_, i) => i + 1).join("|")})$`); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (siblingCheckPattern.test(url) && method === "GET") { + inFlight += 1; + maxInFlight = Math.max(maxInFlight, inFlight); + await new Promise((resolve) => setTimeout(resolve, 5)); // force genuine overlap between concurrent checks + inFlight -= 1; + return Response.json({ state: "open" }); + } + if (url.endsWith(`/issues/${newIssue}`) && method === "PATCH") { closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } + if (url.includes(`/issues/${newIssue}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-bounded-concurrency", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: newIssue, title: "Prolific author's newest issue", state: "open", user: { login: "prolific" }, labels: [], body: "x" }, + }, + }); + + expect(closed).toBe(true); // the over-cap issue is closed, confirming the sibling live-check path actually ran + expect(maxInFlight).toBeGreaterThan(1); // genuinely concurrent, not accidentally serial + expect(maxInFlight).toBeLessThanOrEqual(EXPECTED_LIVE_CHECK_CONCURRENCY); + }); + + it("contributor open-ISSUE cap (#2270): a maintainer-named autoCloseExemptLogins entry is exempt from the PER-REPO issue cap too (not just the install-wide cap)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Sentry issue one", state: "open", user: { login: "sentry[bot]" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Sentry issue two", state: "open", user: { login: "sentry[bot]" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + autonomy: { close: "auto", label: "auto" }, + contributorOpenIssueCap: 2, + autoCloseExemptLogins: ["sentry[bot]"], + }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-exempt-login", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Sentry's 3rd issue", state: "open", user: { login: "sentry[bot]" }, labels: [], body: "x" }, + }, + }); + + // Exempt: the 3rd issue is NOT closed for the cap, despite being (numerically) over it. + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBe(0); + }); + + it("REGRESSION (#2479 gate finding): a stale-open DB row for an already-closed sibling does NOT inflate the count and wrongly close a newly opened issue within the real cap", async () => { + // Issue #60 is stored `open` locally but is ACTUALLY closed on GitHub (live GET returns closed) -- e.g. a + // webhook this instance hasn't processed yet, or a manual close elsewhere. Without live-verifying it, the + // stale count would be 3 (60, 61, 62) against a cap of 2, wrongly closing #62. Live-verified, the real count + // is 2 (61, 62), within cap. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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 }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/60") && method === "GET") return Response.json({ state: "closed" }); + if (url.endsWith("/issues/61") && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-stale-closed-sibling", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's issue, within the real cap", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + }); + + it("REGRESSION (#2479 gate finding, second pass): a live-check failure for a counted sibling fails SAFE (excluded from the count) rather than counting it toward an irreversible close", async () => { + // Unlike reconcileLiveDuplicateSiblings' fail-open-to-stored contract (safe there because it only re-ranks a + // non-final duplicate-cluster winner recomputed every delivery), this count directly gates an IRREVERSIBLE + // close. An unreadable live fetch for sibling #60 (404) must NOT let it keep counting toward the cap -- + // otherwise a transient fetch failure stacked on a stale "open" DB row would wrongly close a newly opened + // issue that is actually within the real cap. #60 unverifiable + #61 confirmed open + #62 incoming = 2, + // within the cap of 2, so #62 must NOT close. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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 }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/60") && method === "GET") return new Response("not found", { status: 404 }); + if (url.endsWith("/issues/61") && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-live-check-fails-safe", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's issue, within the real cap", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + }); + + it("a live-verified-open sibling still counts toward the cap and closes the incoming issue when genuinely over", async () => { + // Positive-confirmation path: both siblings live-verify as open, so the real count (60, 61, 62 = 3) against + // a cap of 2 is genuine, and #62 correctly closes. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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"); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/60") && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/61") && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + if (url.includes("/issues/62/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/62/labels") || url.includes("/issues/62/comments")) return Response.json([], { status: 201 }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-live-verified-genuine", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's genuinely 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(true); + }); + + it("falls back to GITHUB_PUBLIC_TOKEN for the sibling live-check when the installation token mint fails", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITHUB_PUBLIC_TOKEN: "public-fallback-token" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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 }); + const seen = { closed: false, sawPublicToken: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return new Response("suspended", { status: 401 }); + if (url.endsWith("/issues/60") && method === "GET") { + seen.sawPublicToken = new Headers(init?.headers).get("authorization")?.includes("public-fallback-token") ?? false; + return Response.json({ state: "closed" }); + } + if (url.endsWith("/issues/61") && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-public-token-fallback", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's issue, within the real cap", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.sawPublicToken).toBe(true); + // #60 was live-verified closed via the public-token fallback, so the real count (61, 62) is within cap. + expect(seen.closed).toBe(false); + }); + + it("contributor open-ISSUE cap (#2270): disabled (no cap configured, the default) never closes an over-threshold contributor's issue", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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" }); + // No contributorOpenIssueCap set — the default, disabled state. + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-disabled", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + }); + + it("install-wide contributor open-item cap (#2562): an over-install-cap contributor's issue is caught even with NO per-repo contributorOpenIssueCap configured", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); + await upsertIssueFromGitHub(env, "JSONbored/repo-a", { number: 20, title: "Farmer issue on repo-a", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer issue on repo-b", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); + // No contributorOpenIssueCap set — only the install-wide env cap should catch this. + await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" } }); + const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } + if (url.includes("/issues/62/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/62/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } + if (url.includes("/issues/62/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } + // Install-wide live-verify (#2562 gate-review follow-up) re-fetches every OTHER counted sibling before + // trusting it toward the cap -- both of farmer99's other open items must resolve as confirmed-open here. + if (url.endsWith("/repos/JSONbored/repo-a/issues/20")) return Response.json({ number: 20, state: "open" }); + if (url.endsWith("/repos/JSONbored/repo-b/issues/10")) return Response.json({ number: 10, state: "open" }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-issue-cap-close", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 3rd issue install-wide", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(true); + expect(seen.labels).toContain("over-contributor-limit"); + // Install-wide cap counts BOTH open PRs and open issues together (#2562 gate-review follow-up), so the + // close message reports the mixed noun rather than a stale "issues"-only phrasing from the old count-only path. + expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open pull requests and issues") && c.includes("across every repository it gates"))).toBe(true); + }); + + it("install-wide contributor open-item cap (#2562, #4511): env var unset falls back to the real default (20), so an issue author spread across repos well under it is not closed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // no GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP -- resolves to the DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP, not "no cap" (#4511) + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertIssueFromGitHub(env, "JSONbored/repo-a", { number: 20, title: "Farmer issue on repo-a", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer issue on repo-b", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" } }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-issue-cap-off-by-default", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 3rd issue install-wide", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + }); + + it("install-wide contributor open-item cap (#2562): an issue author AT (not over) the install-wide cap is not closed, and falls through to the (unset) per-repo issue cap check safely", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertIssueFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer issue on repo-b", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); + // No contributorOpenIssueCap configured -- exercises the (typeof cap !== "number") early return after the + // install-wide check falls through without matching. + await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" } }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-issue-cap-at-limit", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 2nd issue, at the install-wide limit", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + }); + + it("install-wide contributor open-item cap (#2562): an over-install-cap issue plans no action (observe-only autonomy) and does not execute a close", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [ + { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, + ], + }); + await upsertIssueFromGitHub(env, "JSONbored/repo-a", { number: 20, title: "Farmer issue on repo-a", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); + await upsertIssueFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer issue on repo-b", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); + // autonomy: {} (no acting classes granted) — the plan builds empty, so `planned.length > 0` is false. + await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: {} }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-contributor-issue-cap-observe-only", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 3rd issue install-wide, observe-only", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + }); + + it("install-wide contributor open-item cap (#2562): with BOTH the global cap and the per-repo issue cap configured, an author within the global cap still trips the per-repo cap unchanged", async () => { + // Global cap of 5 is never approached (only 1 open item on repo-b), but the per-repo contributorOpenIssueCap + // of 2 on repo-a IS tripped by this author's 3rd repo-a issue -- proves the two checks are independent and + // the per-repo path still runs (typeof cap !== "number" false branch) after the global check falls through. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "5" }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }], + }); + 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 }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } + if (url.includes("/issues/62/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/62/labels") && method === "POST") return Response.json([]); + if (url.includes("/issues/62/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "global-and-per-repo-issue-cap-both-configured", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 3rd repo-a issue, over the per-repo cap only", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(true); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + }); + + it("contributor open-ISSUE cap (#2270): the repo OWNER's own issue is never closed even over the cap", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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 }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-owner", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Owner's 3rd issue", state: "open", user: { login: "JSONbored" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + }); + + it("contributor open-ISSUE cap (#2270): a contributor's 2nd issue AT (not over) a cap of 2 is not closed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // 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 }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-at-limit", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 2nd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + }); + + it("contributor open-ISSUE cap (#2270): an over-cap issue is not closed when both label and close autonomy are observe-only", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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" }); + // 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 }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-no-autonomy", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n ?? 0).toBe(0); + }); + + it("contributor open-ISSUE cap (#2270): a slash-free repoFullName is safely planned (repoOwner computation guard) even though the GitHub call itself can never succeed against that name", async () => { + // A real webhook always carries "owner/repo"; this pins the DEFENSIVE repoFullName.includes("/") ? ... : "" + // fallback (mirroring the PR path's own such guard) against a malformed value WITHOUT crashing the cap + // computation. The actual close attempt legitimately errors — splitRepo() (shared by every GitHub-action + // primitive) rejects any repoFullName that isn't "owner/repo" — and that error is caught and audited, not + // thrown into the webhook handler; a successful close against a slash-free name is not physically possible + // via the real GitHub REST API, so asserting an audited error (not a crash) is the correct expectation. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }, 123); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }], + }); + 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 }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/issues/60") || url.endsWith("/issues/61")) return Response.json({ state: "open" }); + return Response.json({}); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-noslash", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "", id: 1, type: "User" } }, + repository: { name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }, + issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }), + ).resolves.not.toThrow(); + + const closeAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = 'agent.action.close' order by created_at desc limit 1").first<{ outcome: string; detail: string }>(); + expect(closeAudit?.outcome).toBe("error"); + expect(closeAudit?.detail).toMatch(/Invalid repository full name/); + }); + + it("contributor open-ISSUE cap (#2270): an author-less (ghost) open issue among the repo's others is excluded from the count, not crashed on", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // A ghost issue with no `user` at all (authorLogin ends up null) — must not match farmer99's count nor throw. + 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 }); + const seen = { closed: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); + if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "contributor-issue-cap-ghost-author", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, + }, + }); + + // Ghost issue's null authorLogin never matches "farmer99" — the count is still exactly 3 (farmer99's own), + // so the cap-of-2 close fires; a broken nullish fallback would either crash or double-count the ghost. + expect(seen.closed).toBe(true); + }); + + // #1092: prReadyForReview rebases a BEHIND-base PR through the agent executor (gated by update_branch autonomy + // + pull_requests:write) before reviewing, then defers — the synchronize on the new head re-runs review. +}); diff --git a/test/unit/queue-4.test.ts b/test/unit/queue-4.test.ts new file mode 100644 index 0000000000..5342eb8d77 --- /dev/null +++ b/test/unit/queue-4.test.ts @@ -0,0 +1,7002 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { generateKeyPairSync } from "node:crypto"; +import { clearInstallationTokenCacheForTest } from "../../src/github/app"; +import { clearReviewSuppressionCacheForTest } from "../../src/review/review-memory-wire"; +import { PR_PANEL_COMMENT_MARKER } from "../../src/github/comments"; +import * as backfillModule from "../../src/github/backfill"; +import * as rateLimitModule from "../../src/github/rate-limit"; +import * as repositoriesModule from "../../src/db/repositories"; +import * as reviewEffortModule from "../../src/review/review-effort"; +import * as repositorySettingsModule from "../../src/settings/repository-settings"; +import * as sentryModule from "../../src/selfhost/sentry"; +import { renderMetrics, resetMetrics } from "../../src/selfhost/metrics"; +import { jobCoalesceKey } from "../../src/selfhost/queue-common"; +import { + listCollisionEdges, + createAgentRun, + getCommandUsefulnessSummary, + getBurdenForecast, + getContributorEvidence, + getAgentRun, + getContributorScoringProfile, + getWebhookEvent, + getInstallation, + getLatestUpstreamRulesetSnapshot, + getPullRequest, + getPullRequestDetailSyncState, + upsertPullRequestDetailSyncState, + getRepository, + listUpstreamDriftReports, + listInstallationHealth, + listProductUsageDailyRollups, + listProductUsageEvents, + listPullRequests, + listPullRequestFiles, + listRepoSyncStates, + listSignalSnapshots, + persistSignalSnapshot, + recordGateBlockOutcome, + markGateOutcomeOverridden, + recordProductUsageEvent, + upsertAgentCommandAnswer, + upsertCheckSummary, + upsertIssueFromGitHub, + upsertRepoSyncSegment, + upsertInstallation, + updatePullRequestSlopAssessment, + upsertOfficialMinerDetection, + upsertPullRequestFile, + upsertPullRequestFromGitHub, + upsertIssueWatchSubscription, + upsertRepositoryAiKey, + upsertRepositorySettings, + upsertRepositoryFromGitHub, + putCachedAiReview, + markAiReviewPublished, + putCachedAiSlopAdvisory, + putCachedLinkedIssueSatisfaction, + recordReviewSuppression, + listReviewSuppressions, + setGlobalAgentFrozen, +} from "../../src/db/repositories"; +import { agentMaintenanceHeadMatchesGate, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors"; +import type { PullRequestRecord } from "../../src/types"; +import { aiReviewCacheInputFingerprint } from "../../src/review/ai-review-cache-input"; +import { fingerprint as reviewMemoryFingerprint } from "../../src/review/review-memory-match"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; +import * as focusManifestLoaderModule from "../../src/signals/focus-manifest-loader"; +import { normalizeRegistryPayload } from "../../src/registry/normalize"; +import { persistRegistrySnapshot } from "../../src/registry/sync"; +import { + classifyPullRequestFreshness, + fetchPullRequestFreshness, +} from "../../src/github/pr-freshness"; +import { createTestEnv } from "../helpers/d1"; +import { ISSUE_WAKE_MAX_PRS, MERGE_WAKE_MAX_PRS, SWEEP_MAX_PRS } from "../../src/settings/agent-sweep"; +import { AGENT_LABEL_PENDING_CLOSURE, DEFAULT_LINKED_ISSUE_HARD_RULES } from "../../src/review/linked-issue-hard-rules"; + +vi.mock("../../src/github/pr-freshness", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchPullRequestFreshness: vi.fn(async (_env: Env, args: { expectedHeadSha?: string | null }) => ({ + status: "current" as const, + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [] as string[], + })), + }; +}); + +// The re-gate sweep now FANS OUT the heavy re-review + marker stamp into per-PR `agent-regate-pr` jobs +// (#audit-sweep-fanout). A test asserting the re-review/stamp side effects must run the sweep AND drain the +// per-PR jobs it enqueues. Returns the captured agent-regate-pr jobs for assertions. +async function sweepAndDrainPerPr(env: Env, repoFullName: string): Promise { + const fanned: import("../../src/types").JobMessage[] = []; + const send = env.JOBS.send.bind(env.JOBS); + env.JOBS.send = (async (message: import("../../src/types").JobMessage, options?: QueueSendOptions) => { + if (message.type === "agent-regate-pr") fanned.push(message); + return send(message, options); + }) as typeof env.JOBS.send; + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName }); + env.JOBS.send = send; + for (const job of fanned) await processJob(env, job); + return fanned; +} + + +function completeSegment(repoFullName: string, segment: "labels" | "open_issues" | "open_pull_requests") { + return { + repoFullName, + segment, + status: "complete" as const, + sourceKind: "test" as const, + mode: "resume" as const, + fetchedCount: 1, + expectedCount: 1, + pageCount: 1, + completedAt: "2026-05-25T00:00:00.000Z", + warnings: [], + }; +} + +type CommandAnswerFixture = Parameters[1]; + +function commandAnswer(id: string, command: string, overrides: Partial = {}): CommandAnswerFixture { + return { + id, + repoFullName: "JSONbored/gittensory", + issueNumber: 77, + command, + requestCommentId: 7, + responseCommentId: 9001, + responseUrl: "https://github.com/JSONbored/gittensory/pull/77#issuecomment-9001", + actorKind: "maintainer" as const, + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + metadata: {}, + ...overrides, + }; +} + +function commandAnswerBody(answerId: string, command: string): string { + return [ + "", + ``, + `Command: \`@gittensory ${command}\``, + "Feedback is aggregate-only.", + ].join("\n"); +} + +function queueMinerSnapshot(login: string) { + return { + source: "gittensor_api" as const, + githubId: "123", + githubUsername: login, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + issueDiscoveryScore: 0, + issueTokenScore: 0, + issueCredibility: 1, + isIssueEligible: false, + issueEligibleRepoCount: 0, + alphaPerDay: 0, + taoPerDay: 0, + usdPerDay: 0, + totals: { + pullRequests: 3, + mergedPullRequests: 2, + openPullRequests: 1, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + }, + repositories: [], + pullRequests: [], + issueLabels: [], + }; +} + +function b64(value: string): string { + return Buffer.from(value, "utf8").toString("base64"); +} + +function withProductUsageInsertFailure(env: Env): Env { + const db = env.DB as unknown as { prepare(sql: string): unknown; batch(statements: unknown[]): Promise }; + return { + ...env, + DB: { + prepare(sql: string) { + if (sql.includes("product_usage_events")) throw new Error("product usage insert failed"); + return db.prepare.call(db, sql); + }, + batch(statements: unknown[]) { + return db.batch.call(db, statements); + }, + } as unknown as D1Database, + }; +} + +async function generatePrivateKeyPem(): Promise { + const key = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); + const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; +} + +describe("queue processors", () => { + // Freshness-SLO fixtures are dated relative to late May 2026; pin the clock so staleness windows + // stay deterministic regardless of when CI runs. + beforeEach(() => { + clearInstallationTokenCacheForTest(); + clearReviewSuppressionCacheForTest(); + vi.mocked(fetchPullRequestFreshness).mockReset(); + vi.mocked(fetchPullRequestFreshness).mockImplementation(async (_env, args) => ({ + status: "current", + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [], + })); + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-05-28T00:00:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + async function seedBehindRepo(env: Env, over: { autonomy?: Record; agentPaused?: boolean; perms?: Record; noInstall?: boolean } = {}) { + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + if (!over.noInstall) { + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: over.perms ?? { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + } + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: over.autonomy ?? { merge: "auto", update_branch: "auto" }, + agentPaused: over.agentPaused ?? false, + }); + } + + function behindWebhook() { + return { + type: "github-webhook" as const, + deliveryId: "behind-update-branch", + eventName: "pull_request" as const, + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 48, title: "Behind base", state: "open", user: { login: "contributor" }, head: { sha: "behindsha" }, labels: [], body: "x" }, + }, + }; + } + + it("auto-maintain (#1092): a BEHIND-base PR routes update-branch through the executor, then defers review", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedBehindRepo(env); + let updateBranchCalls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/48/update-branch")) { + updateBranchCalls += 1; + return Response.json({ message: "Updating pull request branch." }, { status: 202 }); + } + if (/\/pulls\/48(?:\?|$)/.test(url)) return Response.json({ number: 48, state: "open", head: { sha: "behindsha" }, mergeable_state: "behind" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, behindWebhook()); + + expect(updateBranchCalls).toBe(1); // the rebase was issued before review + const ub = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.update_branch").first<{ outcome: string }>(); + expect(ub?.outcome).toBe("completed"); + // Deferred for the rebase → no gate verdict published on the stale head. + const merge = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.merge").first<{ n: number }>(); + expect(merge?.n).toBe(0); + }); + + it("auto-maintain (#1092): a behind PR is not rebased when the installation lacks pull_requests:write (falls through)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedBehindRepo(env, { noInstall: true }); + // A stored open PR + the recapture-preview job drive reReviewStoredPullRequest directly (no webhook + // installation upsert), so getInstallation(...) is null → installation?.permissions ?? null hits the null arm. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 48, title: "Behind base", state: "open", user: { login: "contributor" }, head: { sha: "behindsha" }, labels: [], body: "x" }); + let updateBranchCalls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/48/update-branch")) { + updateBranchCalls += 1; + return Response.json({}, { status: 202 }); + } + if (/\/pulls\/48(?:\?|$)/.test(url)) return Response.json({ number: 48, mergeable_state: "behind" }); + // CI still running on the (un-rebased) head → prReadyForReview defers at the CI gate, cleanly. + if (url.includes("/commits/behindsha/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI build", status: "in_progress", conclusion: null }] }); + if (url.includes("/commits/behindsha/status")) return Response.json({ state: "pending", statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { type: "recapture-preview", deliveryId: "rp-48", installationId: 123, repoFullName: "JSONbored/gittensory", prNumber: 48, attempt: 1 }); + + expect(updateBranchCalls).toBe(0); // no installation perms → the executor denies the write; the block falls through + }); + + it("recapture-preview (#1158): a clean PR re-review threads previewPollAttempt into the public-surface publish", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 9101, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "preview-repo", full_name: "owner/preview-repo", private: false, owner: { login: "owner" } }, 9101); + await upsertRepositorySettings(env, { repoFullName: "owner/preview-repo", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/preview-repo", { number: 9, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "c9" }, labels: [], body: "x" }); + await upsertPullRequestFile(env, { repoFullName: "owner/preview-repo", pullNumber: 9, path: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, payload: { patch: "@@\n+export const ok = true;" } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/9/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (/\/pulls\/9(?:\?|$)/.test(url)) return Response.json({ number: 9, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "c9" }, labels: [], body: "x" }); + if (url.includes("/commits/c9/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/c9/status")) return Response.json({ state: "success", statuses: [] }); + return new Response("not found", { status: 404 }); + }); + vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + + // attempt:2 → previewPollAttempt is defined, so the conditional spread at the publish call takes the + // `{ previewPollAttempt }` arm (the recapture-preview poll path; the sweep/webhook callers omit it). + await expect( + processJob(env, { type: "recapture-preview", deliveryId: "rp-9", installationId: 9101, repoFullName: "owner/preview-repo", prNumber: 9, attempt: 2 }), + ).resolves.toBeUndefined(); + }); + + it("recapture-preview (#review-pre-merge-checks): a slop-gated re-review refreshes the PR's files before publishing", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 9102, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "slop-repo", full_name: "owner/slop-repo", private: false, owner: { login: "owner" } }, 9102); + // slopGateMode != "off" ⇒ shouldCollectSlopEvidence(settings) is true ⇒ reReviewStoredPullRequest enters the + // refresh branch (the file-refresh body), so the stored files reflect the PR's current head before publishing. + await upsertRepositorySettings(env, { repoFullName: "owner/slop-repo", checkRunMode: "off", commentMode: "off", publicSurface: "off", slopGateMode: "advisory" }); + await upsertPullRequestFromGitHub(env, "owner/slop-repo", { number: 11, title: "Slop PR", state: "open", user: { login: "contributor" }, head: { sha: "s11" }, labels: [], body: "x" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/11/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (/\/pulls\/11(?:\?|$)/.test(url)) return Response.json({ number: 11, title: "Slop PR", state: "open", user: { login: "contributor" }, head: { sha: "s11" }, labels: [], body: "x" }); + if (url.includes("/commits/s11/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/s11/status")) return Response.json({ state: "success", statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + await expect( + processJob(env, { type: "recapture-preview", deliveryId: "rp-11", installationId: 9102, repoFullName: "owner/slop-repo", prNumber: 11, attempt: 1 }), + ).resolves.toBeUndefined(); + + // refreshPullRequestDetails ran ⇒ a detail-sync-state row was written for this PR (the if-body executed). + const sync = await env.DB.prepare("select status from pull_request_detail_sync_state where repo_full_name = ? and pull_number = ?").bind("owner/slop-repo", 11).first<{ status: string }>(); + expect(sync?.status).toMatch(/^(complete|partial)$/); + }); + + it("auto-maintain (#778): a repo with no acting autonomy takes no agent action", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + requireLinkedIssue: true, + autonomy: { label: "observe" }, // not acting → agent never runs + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/gate123/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "no-autonomy", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 43, title: "No issue", state: "open", user: { login: "contributor" }, head: { sha: "gate123" }, labels: [], body: "No issue link." }, + }, + }); + + const count = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'agent.action.%'").first<{ n: number }>(); + expect(count?.n).toBe(0); + }); + + it("auto-maintain (#778): takes no terminal action when merge/close/approve autonomy is not granted (gate now fails normally for a non-confirmed author)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { review_state_label: "auto", request_changes: "auto" }, + }); + // No confirmed-miner seed → author is unconfirmed; the manifest's linkedIssue:block + no issue fires a + // blocker, so the gate now FAILS the author normally (#gate-nonconfirmed — confirmed status no longer + // neutralizes the verdict). But this repo grants only review_state_label/request_changes autonomy — NOT + // merge/close/approve — so the failing gate yields a request-changes/label action at most, never a terminal action. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/gate123/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "unconfirmed", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 45, title: "No issue", state: "open", user: { login: "stranger" }, head: { sha: "gate123" }, labels: [], body: "No issue link." }, + }, + }); + + // The failing gate is surfaced (request-changes/label), but with no merge/close/approve autonomy granted the + // bot takes NO TERMINAL action — proving terminal actions require their own autonomy grant, independent of the + // gate verdict. (Auto-close on a failing gate is exercised by the #778 close-autonomy tests below.) + const terminal = await env.DB.prepare("select count(*) as n from audit_events where event_type in ('agent.action.merge','agent.action.close','agent.action.approve')").first<{ n: number }>(); + expect(terminal?.n).toBe(0); + }); + + it("auto-maintain (#778): skips a closed PR even on an agent-configured repo", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { label: "auto" }, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); + if (url.includes("/comments")) return Response.json({ id: 1 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "closed-pr", + eventName: "pull_request", + payload: { + action: "closed", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 46, title: "Closed", state: "closed", user: { login: "contributor" }, head: { sha: "gate123" }, labels: [], body: "x" }, + }, + }); + + const count = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'agent.action.%'").first<{ n: number }>(); + expect(count?.n).toBe(0); + }); + + it("auto-maintain (#778): labels a clean passing PR even with no author and no installation record (dry-run)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + // No installation record seeded → installation lookup returns null (label needs only issues:write, exempt). + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { review_state_label: "auto" }, + agentDryRun: true, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/clean123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/clean123/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "no-author-clean", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + // No `user` → authorLogin is absent; default linkedIssue mode is advisory so the verdict is a clean pass. + pull_request: { number: 47, title: "Clean", state: "open", head: { sha: "clean123" }, labels: [], body: "Closes #1" }, + }, + }); + + const labelAudit = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.action.label").first<{ outcome: string; metadata_json: string }>(); + expect(labelAudit?.outcome).toBe("completed"); + expect(JSON.parse(labelAudit?.metadata_json ?? "{}")).toMatchObject({ mode: "dry_run" }); + }); + + it("publishes an enabled gate when bot PR public output is skipped", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + }); + const calls = { gateChecks: 0, comments: 0, minerList: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gatebot123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/issues/53/comments")) { + calls.comments += 1; + return Response.json([]); + } + if (url.includes("/check-runs") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); + expect(body.conclusion).toBeUndefined(); + calls.gateChecks += 1; + return Response.json({ id: 910 }, { status: 201 }); + } + if (url.includes("/check-runs/910") && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + // The bot author is gated normally now (no confirmation gate); linked-issue block + no issue → failure (#gate-nonconfirmed). + expect(body).toMatchObject({ status: "completed", conclusion: "failure", output: { title: "Gittensory Orb Review Agent: No linked issue detected" } }); + calls.gateChecks += 1; + return Response.json({ id: 910 }); + } + return new Response("not found", { status: 404 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-bot-public-skip", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 53, title: "Bot PR", state: "open", user: { login: "automation-bot", type: "Bot" }, head: { sha: "gatebot123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ gateChecks: 2, comments: 0, minerList: 0 }); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#53") + .first<{ detail: string }>(); + expect(audit?.detail).toBe("bot_author"); + }); + + it("evaluates the gate while suppressing public review output for ignored authors", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + }); + const calls = { gateChecks: 0, comments: 0, minerList: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/ignoredauthor123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/issues/56/comments")) { + if (method !== "GET") calls.comments += 1; + return Response.json([]); + } + if (url.includes("/check-runs") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); + expect(body.conclusion).toBeUndefined(); + calls.gateChecks += 1; + return Response.json({ id: 930 }, { status: 201 }); + } + if (url.includes("/check-runs/930") && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ + status: "completed", + conclusion: "failure", + output: { title: "Gittensory Orb Review Agent: No linked issue detected" }, + }); + calls.gateChecks += 1; + return Response.json({ id: 930 }); + } + return new Response("not found", { status: 404 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { + gate: { linkedIssue: "block" }, + review: { auto_review: { ignore_authors: ["renovate*"] } }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "ignored-author-skip", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 56, title: "Automated dependency update", state: "open", user: { login: "renovate-release" }, head: { sha: "ignoredauthor123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ gateChecks: 2, comments: 1, minerList: 0 }); + const visibilitySkip = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#56") + .first<{ detail: string; metadata_json: string }>(); + expect(visibilitySkip?.detail).toBe("ignored_author"); + expect(JSON.parse(visibilitySkip?.metadata_json ?? "{}")).toMatchObject({ deliveryId: "ignored-author-skip" }); + }); + + it("audits ignored authors without a skipped check when review checks are disabled", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + reviewCheckMode: "disabled", + linkedIssueGateMode: "off", + }); + const calls = { github: 0, minerList: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") calls.minerList += 1; + if (url.includes("api.github.com")) calls.github += 1; + return new Response("not found", { status: 404 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { + review: { auto_review: { ignore_authors: ["release-please*"] } }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "ignored-author-no-check", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 57, title: "Automated release", state: "open", user: { login: "release-please-bot" }, head: { sha: "ignorednocheck123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ github: 0, minerList: 0 }); + const skipped = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#57") + .first<{ detail: string; metadata_json: string }>(); + expect(skipped?.detail).toBe("ignored_author"); + expect(JSON.parse(skipped?.metadata_json ?? "{}")).toMatchObject({ deliveryId: "ignored-author-no-check" }); + }); + + it("keeps surface_off precedence over ignored authors when no PR surface is visible", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + reviewCheckMode: "disabled", + linkedIssueGateMode: "off", + }); + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { + review: { auto_review: { ignore_authors: ["renovate*"] } }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "surface-off-before-ignored-author", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 58, title: "Automated dependency update", state: "open", user: { login: "renovate-release" }, head: { sha: "surfaceoff123" }, labels: [], body: "No issue link." }, + }, + }); + + const skips = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ? order by created_at") + .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#58") + .all<{ detail: string }>(); + expect(skips.results.map((row) => row.detail)).toEqual(["surface_off"]); + const publicSkip = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_public_surface_skipped", "JSONbored/gittensory#58") + .first<{ detail: string }>(); + expect(publicSkip ?? null).toBeNull(); + }); + + it("publishes an enabled gate when Gittensor-only public output is skipped for an unconfirmed miner", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicAudienceMode: "gittensor_only", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + }); + const calls = { minerList: 0, gateChecks: 0, comments: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gateminer123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/issues/54/comments")) { + calls.comments += 1; + return Response.json([]); + } + if (url.includes("/check-runs") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); + expect(body.conclusion).toBeUndefined(); + calls.gateChecks += 1; + return Response.json({ id: 920 }, { status: 201 }); + } + if (url.includes("/check-runs/920") && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + // The unconfirmed miner is gated normally now; linked-issue block + no issue → failure (#gate-nonconfirmed). + expect(body).toMatchObject({ status: "completed", conclusion: "failure", output: { title: "Gittensory Orb Review Agent: No linked issue detected" } }); + calls.gateChecks += 1; + return Response.json({ id: 920 }); + } + return new Response("not found", { status: 404 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-unconfirmed-miner-public-skip", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 54, title: "Unconfirmed miner PR", state: "open", user: { login: "newbie" }, head: { sha: "gateminer123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ minerList: 1, gateChecks: 2, comments: 0 }); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#54") + .first<{ detail: string }>(); + expect(audit?.detail).toBe("not_official_gittensor_miner"); + }); + + it("keeps gate checks without double-auditing unavailable miner detection as not official", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicAudienceMode: "gittensor_only", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + }); + + const calls = { minerList: 0, gateChecks: 0, comments: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return new Response("gittensor unavailable", { status: 503 }); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gateunavailable123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/issues/55/comments")) { + calls.comments += 1; + return Response.json([]); + } + if (url.includes("/check-runs") && method === "POST") { + calls.gateChecks += 1; + return Response.json({ id: 921 }, { status: 201 }); + } + if (url.includes("/check-runs/921") && method === "PATCH") { + calls.gateChecks += 1; + return Response.json({ id: 921 }); + } + return new Response("not found", { status: 404 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-unavailable-miner-public-skip", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 55, title: "Unavailable miner PR", state: "open", user: { login: "newbie" }, head: { sha: "gateunavailable123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ minerList: 1, gateChecks: 2, comments: 0 }); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ? order by id") + .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#55") + .all<{ detail: string }>(); + expect(audit.results.map((event) => event.detail)).toEqual(["miner_detection_unavailable"]); + }); + + it("hard-blocks a confirmed Gittensor contributor in a gate-only configuration when a configured blocker fires", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicAudienceMode: "oss_maintainer", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + }); + const calls = { minerList: 0, gateChecks: 0 }; + let gatePatchBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([ + { uid: 7, githubUsername: "confirmed-dev", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/confirmed123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs/940") && method === "PATCH") { + gatePatchBody = JSON.parse(String(init?.body ?? "{}")) as typeof gatePatchBody; + calls.gateChecks += 1; + return Response.json({ id: 940 }); + } + if (url.includes("/check-runs") && method === "POST") { + calls.gateChecks += 1; + return Response.json({ id: 940 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-confirmed-block", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 61, title: "Add helper", state: "open", user: { login: "confirmed-dev" }, head: { sha: "confirmed123" }, labels: [], body: "Adds a helper." }, + }, + }); + + // A confirmed contributor with a configured hard blocker (linked-issue gate set to block, no issue + // linked) IS blocked even when the Gate is the only public output, and the Gate names the exact + // blocker so the fix is obvious. + expect(calls.minerList).toBe(1); + expect(calls.gateChecks).toBe(2); + expect(gatePatchBody.conclusion).toBe("failure"); + expect(gatePatchBody.output?.title).toBe("Gittensory Orb Review Agent: No linked issue detected"); + }); + + it("hard-blocks a confirmed contributor on a dual-model AI consensus defect when aiReview: block is opted in", async () => { + const defectJson = JSON.stringify({ + assessment: "Introduces a likely crash.", + blockers: ["Unhandled null dereference on empty input in src/a.ts — the new branch dereferences a possibly-null value."], + nits: ["Guard the null case."], + suggestions: ["Guard the null case."], + }); + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: defectJson }) } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "block", + // Also exercise the opt-in slop advisory in the same surface pass: it persists a per-PR assessment + // and runs the (advisory-only) AI slop pass, but never blocks — the gate still fails on the AI + // consensus defect alone. + slopGateMode: "advisory", + slopAiAdvisory: true, + }); + let gatePatchBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; + const cacheReadSpy = vi + .spyOn(repositoriesModule, "getCachedAiReview") + .mockRejectedValueOnce(new Error("cache read failed")); + const cacheWriteSpy = vi + .spyOn(repositoriesModule, "putCachedAiReview") + .mockRejectedValueOnce(new Error("cache write failed")); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { uid: 7, githubUsername: "confirmed-dev", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/aidefect123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs/950") && method === "PATCH") { + gatePatchBody = JSON.parse(String(init?.body ?? "{}")) as typeof gatePatchBody; + return Response.json({ id: 950 }); + } + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 950 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-ai-consensus-block", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 71, title: "Add helper", state: "open", user: { login: "confirmed-dev" }, head: { sha: "aidefect123" }, labels: [], body: "Adds a helper." }, + }, + }); + + expect(gatePatchBody.conclusion).toBe("failure"); + expect(gatePatchBody.output?.title).toContain("AI reviewers agree on a likely critical defect"); + // The AI usage event was recorded for the review (never with key material). + const usage = await env.DB.prepare("select feature, status from ai_usage_events where feature = ?").bind("ai_review_pr").first<{ feature: string; status: string }>(); + expect(usage).toMatchObject({ feature: "ai_review_pr", status: "ok" }); + expect(cacheReadSpy).toHaveBeenCalled(); + expect(cacheReadSpy.mock.calls[0]?.[5]).toMatch(/^ai-review-input:v4:/); + expect(cacheWriteSpy).toHaveBeenCalled(); + expect(cacheWriteSpy.mock.calls[0]?.[5]).toMatchObject({ + metadata: { inputFingerprint: expect.stringMatching(/^ai-review-input:v4:/) }, + }); + cacheReadSpy.mockRestore(); + cacheWriteSpy.mockRestore(); + }); + + it("finalizes the Gate to neutral instead of leaving it in_progress when gate completion fails", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + }); + const patchBodies: Array<{ status?: string; conclusion?: string; output?: { title?: string } }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/finalize123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 970 }, { status: 201 }); // pending + if (url.includes("/check-runs/970") && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + patchBodies.push(body); + // First PATCH = the gate completion; fail it transiently so the catch must finalize the check. + if (patchBodies.length === 1) return new Response(JSON.stringify({ message: "server error" }), { status: 500 }); + return Response.json({ id: 970 }); + } + return new Response("not found", { status: 404 }); + }); + const realPrepare = env.DB.prepare.bind(env.DB); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + env.DB.prepare = ((sql: string) => { + if (/insert\s+into\s+["`]?check_summaries["`]?/i.test(sql)) throw new Error("summary write failed"); + return realPrepare(sql); + }) as typeof env.DB.prepare; + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-finalize-on-error", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 80, title: "Some change", state: "open", user: { login: "contributor" }, head: { sha: "finalize123" }, labels: [], body: "No issue link." }, + }, + }); + + // The completion PATCH failed (500), so the LOCAL check-run catch finalized the SAME check run (id 970) to + // a neutral, non-blocking terminal state — never left hanging in_progress — and CONTINUED the review + // (no re-throw), so the comment/audit/auto-action still run instead of the whole review dead-lettering. + expect(patchBodies.length).toBe(2); + const finalize = patchBodies[1]; + expect(finalize?.status).toBe("completed"); + expect(finalize?.conclusion).toBe("neutral"); + expect(finalize?.output?.title).toBe("Gittensory Orb Review Agent — could not finish evaluating"); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? and target_key = ?") + .bind("github_app.gate_check_failed_nonfatal", "JSONbored/gittensory#80") + .first<{ outcome: string }>(); + expect(audit?.outcome).toBe("error"); + expect(errors.mock.calls.some((call) => String(call[0]).includes("gate_check_summary_upsert_failed"))).toBe(true); + errors.mockRestore(); + }); + + it("does not stamp a current public surface when a required Gate check never finalizes", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + let commentPosts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-missing/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 972 }, { status: 201 }); + if (url.includes("/check-runs/972") && method === "PATCH") return new Response("check update failed", { status: 500 }); + if (url.includes("/issues/82/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/82/comments") && method === "POST") { + commentPosts += 1; + return Response.json({ id: 8200, html_url: "https://github.com/comment/8200" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-missing-but-comment-posted", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 82, title: "Comment cannot mask missing gate", state: "open", user: { login: "contributor" }, head: { sha: "gate-missing" }, labels: [], body: "No issue link." }, + }, + }); + + expect(commentPosts).toBeGreaterThan(0); + const stored = await getPullRequest(env, "JSONbored/gittensory", 82); + expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); + const incomplete = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_incomplete") + .first<{ detail: string; metadata_json: string }>(); + expect(incomplete?.detail).toBe("required gate check did not finalize"); + expect(incomplete?.metadata_json).toContain('"publishedOutputs":["comment"]'); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_published") + .all(); + expect(published.results).toEqual([]); + const summary = await env.DB.prepare("select id from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") + .bind("JSONbored/gittensory", 82, "gate-missing") + .first<{ id: string }>(); + expect(summary ?? null).toBeNull(); + }); + + it("records the intended label in incomplete-surface audits when a label publishes but Gate never finalizes", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + let labelPosts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-missing-label/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 978 }, { status: 201 }); + if (url.includes("/check-runs/978") && method === "PATCH") return new Response("check update failed", { status: 500 }); + if (url.includes("/issues/88/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/88/labels") && method === "POST") { + labelPosts += 1; + return Response.json([{ name: "gittensor" }]); + } + if (url.includes("/labels") && method === "POST") return Response.json({ name: "gittensor" }, { status: 201 }); + if (url.includes("/labels/") && method === "DELETE") return new Response(null, { status: 204 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-missing-label-published", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 88, title: "Label cannot mask missing gate", state: "open", user: { login: "contributor" }, head: { sha: "gate-missing-label" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(labelPosts).toBeGreaterThan(0); + const stored = await getPullRequest(env, "JSONbored/gittensory", 88); + expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); + const incomplete = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_incomplete") + .first<{ metadata_json: string }>(); + const metadata = JSON.parse(incomplete?.metadata_json ?? "{}"); + expect(metadata).toMatchObject({ + label: "gittensor", + publishedOutputs: ["label"], + }); + }); + + it("does not stamp a gate-only surface when the incomplete-surface audit write fails", async () => { + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + let incompleteAuditWrites = 0; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.pr_public_surface_incomplete") { + incompleteAuditWrites += 1; + throw new Error("audit failed"); + } + await originalRecordAuditEvent(auditEnv, event); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-zero-missing/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 973 }, { status: 201 }); + if (url.includes("/check-runs/973") && method === "PATCH") return new Response("check update failed", { status: 500 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-missing-zero-output", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 83, title: "Gate only missing", state: "open", user: { login: "contributor" }, head: { sha: "gate-zero-missing" }, labels: [], body: "No issue link." }, + }, + }); + + expect(incompleteAuditWrites).toBe(1); + const stored = await getPullRequest(env, "JSONbored/gittensory", 83); + expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); + auditSpy.mockRestore(); + }); + + it("does not stamp a comment surface when the incomplete-surface audit write fails", async () => { + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + let incompleteAuditWrites = 0; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.pr_public_surface_incomplete") { + incompleteAuditWrites += 1; + throw new Error("audit failed"); + } + await originalRecordAuditEvent(auditEnv, event); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate-comment-missing/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 974 }, { status: 201 }); + if (url.includes("/check-runs/974") && method === "PATCH") return new Response("check update failed", { status: 500 }); + if (url.includes("/issues/84/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/84/comments") && method === "POST") return Response.json({ id: 8400, html_url: "https://github.com/comment/8400" }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-missing-comment-audit-fails", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 84, title: "Comment missing gate", state: "open", user: { login: "contributor" }, head: { sha: "gate-comment-missing" }, labels: [], body: "No issue link." }, + }, + }); + + expect(incompleteAuditWrites).toBe(1); + const stored = await getPullRequest(env, "JSONbored/gittensory", 84); + expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); + auditSpy.mockRestore(); + }); + + it("propagates a rate-limited Gate completion so the queue retries and the pending Gate stays reviewing", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + }); + const patchBodies: Array<{ status?: string; conclusion?: string; output?: { title?: string } }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/forbidden403/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 971 }, { status: 201 }); // pending in_progress + if (url.includes("/check-runs/971") && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + patchBodies.push(body); + // Gate completion stays rate-limited through the inline retry budget. It must propagate to the queue instead + // of being swallowed as nonfatal; the pending check remains in_progress while the queue backs off and retries. + return new Response(JSON.stringify({ message: "You have exceeded a secondary rate limit" }), { status: 403, headers: { "retry-after": "0" } }); + } + return new Response("not found", { status: 404 }); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "gate-finalize-on-403", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 81, title: "Some change", state: "open", user: { login: "contributor" }, head: { sha: "forbidden403" }, labels: [], body: "No issue link." }, + }, + }), + ).rejects.toThrow(/rate limit/i); + + expect(patchBodies).toHaveLength(4); // initial attempt + GITHUB_RATE_LIMIT_MAX_RETRIES (3) + expect(patchBodies[0]?.status).toBe("completed"); + }); + + it("disables the gate from .gittensory.yml (gate.enabled: false) even when repo settings enable it", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", + requireLinkedIssue: true, + }); + // Config turns the gate OFF even though repo settings have gateCheckMode: enabled. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { enabled: false } }); + const calls = { gateChecks: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs")) { + calls.gateChecks += 1; + return Response.json({ id: 999 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-yml-disabled", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 70, title: "No issue", state: "open", user: { login: "contributor" }, head: { sha: "ymldisabled123" }, labels: [], body: "No issue." }, + }, + }); + + // gate.enabled: false in .gittensory.yml disables the gate entirely — no Gate check is posted. + expect(calls.gateChecks).toBe(0); + }); + + it("audits opt-in gate check permission failures without blocking webhook processing", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + requireLinkedIssue: true, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gate403/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs")) return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-permission-missing", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 42, title: "Gate without issue", state: "open", user: { login: "contributor" }, head: { sha: "gate403" }, labels: [], body: "No issue link." }, + }, + }); + + const audit = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") + .bind("github_app.gate_check_permission_missing") + .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); + + expect(audit).toMatchObject({ + event_type: "github_app.gate_check_permission_missing", + actor: "contributor", + target_key: "JSONbored/gittensory#42", + outcome: "error", + }); + expect(audit?.detail).toMatch(/Checks: write permission is missing/i); + }); + + it("marks closed PR gates skipped without creating late first comments", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + }); + const calls = { gateWrites: 0, commentGets: 0, commentPosts: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/closed123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string; status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ name: "Gittensory Orb Review Agent", status: "completed", conclusion: "skipped", output: { title: "Gittensory Orb Review Agent skipped" } }); + calls.gateWrites += 1; + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/issues/43/comments") && method === "GET") { + calls.commentGets += 1; + return Response.json([]); + } + if (url.includes("/issues/43/comments") && method === "POST") { + calls.commentPosts += 1; + return Response.json({ id: 1 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-closed", + eventName: "pull_request", + payload: { + action: "closed", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 43, title: "Fast merged PR", state: "closed", user: { login: "contributor" }, head: { sha: "closed123" }, labels: [], body: "Fixes #1" }, + }, + }); + + // The real review is PRESERVED on close: the gate check is marked skipped (gateWrites:1), but the unified + // comment is NOT touched (commentGets:0, commentPosts:0) — no post-close pass overwrites the open-time review + // with an empty skip card. (#preserve-review-on-close) + expect(calls).toEqual({ gateWrites: 1, commentGets: 0, commentPosts: 0 }); + }); + + it("audits closed PR skipped gate permission failures (no late panel write — the real review is preserved)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + }); + let commentGets = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs")) return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); + if (url.includes("/issues/47/comments")) { + commentGets += 1; + return new Response("comments down", { status: 503 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-closed-permission-missing", + eventName: "pull_request", + payload: { + action: "closed", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 47, title: "Fast merged PR", state: "closed", user: { login: "contributor" }, head: { sha: "closed403" }, labels: [], body: "Fixes #1" }, + }, + }); + + // No late panel update on close (the real review is preserved), so the comment endpoint is never hit. + expect(commentGets).toBe(0); + const audit = await env.DB.prepare("select target_key, outcome, detail from audit_events where event_type = ?") + .bind("github_app.gate_check_permission_missing") + .first<{ target_key: string; outcome: string; detail: string }>(); + expect(audit).toMatchObject({ + target_key: "JSONbored/gittensory#47", + outcome: "error", + }); + expect(audit?.detail).toMatch(/Checks: write permission is missing/i); + const webhook = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("gate-closed-permission-missing").first<{ status: string }>(); + expect(webhook?.status).toBe("processed"); + }); + + it("reruns the sticky PR panel when a maintainer checks the rerun task", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicAudienceMode: "oss_maintainer", + publicSignalLevel: "standard", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + includeMaintainerAuthors: true, + commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { "review-now": ["maintainer"] } }, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 45, + title: "Refresh panel", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "panel123" }, + labels: [], + body: "Validation: npm test", + }); + const checkedPanel = [ + "", + "", + "- [x] Re-run Gittensory review", + ].join("\n"); + const calls = { token: 0, permission: 0, minerList: 0, commentGets: 0, commentPatches: 0, checkRuns: 0 }; + let patchedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + // A confirmed official Gittensor contributor → the rerun renders the FULL readiness panel + // (which carries the rerun task); a non-registered author would get the minimal invite. + return Response.json([ + { uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + if (url.includes("/check-runs")) { + calls.checkRuns += 1; + return Response.json({ id: 888 }); + } + if (url.includes("/collaborators/maintainer/permission")) { + calls.permission += 1; + return Response.json({ permission: "maintain" }); + } + if (url.includes("/issues/45/comments") && method === "GET") { + calls.commentGets += 1; + return Response.json([{ id: 777, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); + } + if (url.includes("/issues/comments/777") && method === "PATCH") { + calls.commentPatches += 1; + patchedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 777 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-retrigger", + eventName: "issue_comment", + payload: { + action: "edited", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 45, title: "Refresh panel", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 777, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + // token: 1 — the installation token is now cached + reused within the request (was 2: main + permission check). + // commentGets/commentPatches: 2 — first the purple reviewing placeholder, then the final refreshed panel. + expect(calls).toEqual({ token: 1, permission: 1, minerList: 1, commentGets: 2, commentPatches: 2, checkRuns: 0 }); + expect(patchedBody).toContain(""); + expect(patchedBody).toContain("Readiness score:"); + expect(patchedBody).toContain("- [ ] Re-run Gittensory review"); + expect(patchedBody).not.toContain("- [x] "); + const audit = await env.DB.prepare("select event_type, actor, target_key, outcome from audit_events where event_type = ?") + .bind("github_app.pr_panel_retriggered") + .first<{ event_type: string; actor: string; target_key: string; outcome: string }>(); + expect(audit).toMatchObject({ + event_type: "github_app.pr_panel_retriggered", + actor: "maintainer", + target_key: "JSONbored/gittensory#45", + outcome: "completed", + }); + const usageEvents = await listProductUsageEvents(env, { limit: 5 }); + expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ surface: "github_app", eventName: "pr_panel_retriggered", outcome: "completed" })])); + }); + + it("defers a manual panel rerun while CI is still running", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + includeMaintainerAuthors: true, + autonomy: { merge: "auto" }, + commandAuthorization: { default: ["maintainer"], commands: { "review-now": ["maintainer"] } }, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 46, + title: "Pending CI rerun", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "pendingci" }, + base: { ref: "main" }, + labels: [], + body: "Validation: npm test", + }); + const checkedPanel = [ + "", + "", + "- [x] Re-run Gittensory review", + ].join("\n"); + env.SELFHOST_TRANSIENT_CACHE = { + get: async () => { + throw new Error("Redis unavailable"); + }, + set: async () => undefined, + }; + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.pr_panel_retrigger_deferred") + throw new Error("D1 audit failed"); + await originalRecordAuditEvent(auditEnv, event); + }); + let commentPatches = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); + if (/\/pulls\/46(?:\?|$)/.test(url)) return Response.json({ number: 46, mergeable_state: "clean" }); + if (url.includes("/commits/pendingci/check-runs")) { + return Response.json({ check_runs: [{ name: "test", status: "in_progress", conclusion: null, app: { slug: "github-actions" } }] }); + } + if (url.includes("/commits/pendingci/status")) return Response.json({ statuses: [] }); + if (url.includes("/issues/comments/778") && method === "PATCH") { + commentPatches += 1; + return Response.json({ id: 778 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-retrigger-ci-pending", + eventName: "issue_comment", + payload: { + action: "edited", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 46, title: "Pending CI rerun", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(commentPatches).toBe(0); + expect(auditSpy).toHaveBeenCalledWith( + env, + expect.objectContaining({ + eventType: "github_app.pr_panel_retrigger_deferred", + actor: "maintainer", + targetKey: "JSONbored/gittensory#46", + outcome: "queued", + }), + ); + auditSpy.mockRestore(); + }); + + it("refreshes the PR's files on a manual rerun so the slop/manifest gate evaluates the current diff", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicAudienceMode: "oss_maintainer", + publicSignalLevel: "standard", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + includeMaintainerAuthors: true, + // Slop gate on → the rerun must refresh the PR files before evaluating (the guard fires). + slopGateMode: "advisory", + commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { "review-now": ["maintainer"] } }, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 45, + title: "Refresh panel", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "panel123" }, + labels: [], + body: "Validation: npm test", + }); + const checkedPanel = ["", "", "- [x] Re-run Gittensory review"].join("\n"); + const calls = { pullsFiles: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([{ uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); + // The refresh fetches files/reviews/checks; count the files fetch to prove the refresh ran on the rerun. + if (url.includes("/pulls/45/files")) { + calls.pullsFiles += 1; + return Response.json([{ filename: "src/app.ts", status: "modified", additions: 5, deletions: 1, changes: 6 }]); + } + if (url.includes("/pulls/45/reviews")) return Response.json([]); + if (url.includes("/commits/panel123/check-runs")) return Response.json({ check_runs: [] }); + if (url.includes("/issues/45/comments") && method === "GET") return Response.json([{ id: 777, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); + if (url.includes("/issues/comments/777") && method === "PATCH") return Response.json({ id: 777 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-retrigger-refresh", + eventName: "issue_comment", + payload: { + action: "edited", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 45, title: "Refresh panel", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 777, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + // The rerun fetched the PR's current files before publishing the panel/gate — not the stale cache. + expect(calls.pullsFiles).toBeGreaterThanOrEqual(1); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_panel_retriggered", "JSONbored/gittensory#45") + .first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + }); + + it("skips PR panel reruns from confirmed-miner PR authors because the checkbox is maintainer-only", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + includeMaintainerAuthors: true, + // Even if repo config tries to allow confirmed miners, the checkbox is a maintainer/write-collaborator + // control because it mutates the bot's persisted review comment. + commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { "review-now": ["maintainer", "confirmed_miner"] } }, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 48, + title: "Miner self-rerun", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "panel480" }, + labels: [], + body: "Validation: npm test", + }); + const checkedPanel = ["", "", "- [x] Re-run Gittensory review"].join("\n"); + const calls = { minerList: 0, permission: 0, commentPatches: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([{ uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // The confirmed-miner author has NO repo write/admin — authorized via confirmed_miner, not maintainer. + if (url.includes("/collaborators/contributor/permission")) { + calls.permission += 1; + return Response.json({ permission: "none" }); + } + if (url.includes("/issues/48/comments") && method === "GET") return Response.json([{ id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); + if (url.includes("/issues/comments/778") && method === "PATCH") { + calls.commentPatches += 1; + return Response.json({ id: 778 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-retrigger-miner", + eventName: "issue_comment", + payload: { + action: "edited", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 48, title: "Miner self-rerun", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "contributor", type: "User" }, + }, + }); + + // The checkbox authorization ignores the widened repo command policy, so it never reaches miner detection or + // comment mutation for a plain PR author. + expect(calls.minerList).toBe(0); + expect(calls.permission).toBe(1); + expect(calls.commentPatches).toBe(0); + const audit = await env.DB.prepare("select actor, outcome, detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_panel_retrigger_skipped", "JSONbored/gittensory#48") + .first<{ actor: string; outcome: string; detail: string }>(); + expect(audit).toMatchObject({ actor: "contributor", outcome: "completed", detail: "maintainer_command_requires_maintainer" }); + }); + + it("skips PR panel reruns from users without repository write permission", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 46, + title: "Unauthorized panel refresh", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "panel-denied" }, + labels: [], + body: "Validation: npm test", + }); + const checkedPanel = [ + "", + "", + "- [x] Re-run Gittensory review", + ].join("\n"); + const calls = { token: 0, permission: 0, commentGets: 0, commentPatches: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + if (url.includes("/collaborators/drive-by-user/permission")) { + calls.permission += 1; + return Response.json({ permission: "read" }); + } + if (url.includes("/issues/46/comments")) { + calls.commentGets += 1; + return Response.json([{ id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); + } + if (url.includes("/issues/comments/778")) { + calls.commentPatches += 1; + return Response.json({ id: 778 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-retrigger-denied", + eventName: "issue_comment", + payload: { + action: "edited", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 46, title: "Unauthorized panel refresh", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "drive-by-user", type: "User" }, + }, + }); + + expect(calls).toEqual({ token: 1, permission: 1, commentGets: 0, commentPatches: 0 }); + const audit = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") + .bind("github_app.pr_panel_retrigger_skipped") + .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); + expect(audit).toMatchObject({ + event_type: "github_app.pr_panel_retrigger_skipped", + actor: "drive-by-user", + target_key: "JSONbored/gittensory#46", + outcome: "completed", + detail: "not_maintainer_or_pr_author", + }); + }); + + it("reruns the sticky PR panel when a write collaborator checks the rerun task", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicAudienceMode: "oss_maintainer", + publicSignalLevel: "standard", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + includeMaintainerAuthors: true, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 47, + title: "Refresh panel as collaborator", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "panel-writer" }, + labels: [], + body: "Validation: npm test", + }); + const checkedPanel = [ + "", + "", + "- [x] Re-run Gittensory review", + ].join("\n"); + const calls = { token: 0, permission: 0, minerList: 0, commentGets: 0, commentPatches: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([]); + } + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + if (url.includes("/collaborators/writer/permission")) { + calls.permission += 1; + return Response.json({ permission: "write" }); + } + if (url.includes("/issues/47/comments") && method === "GET") { + calls.commentGets += 1; + return Response.json([{ id: 779, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); + } + if (url.includes("/issues/comments/779") && method === "PATCH") { + calls.commentPatches += 1; + return Response.json({ id: 779 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-retrigger-writer", + eventName: "issue_comment", + payload: { + action: "edited", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 47, title: "Refresh panel as collaborator", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 779, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "writer", type: "User" }, + }, + }); + + // token: 1 — the installation token is now cached + reused within the request (was 2: main + permission check). + // commentGets/commentPatches: 2 — first the purple reviewing placeholder, then the final refreshed panel. + expect(calls).toEqual({ token: 1, permission: 1, minerList: 1, commentGets: 2, commentPatches: 2 }); + }); + + it("skips PR panel reruns when the editing actor and PR author are unavailable", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 48, + title: "Unknown panel refresh actor", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "panel-unknown" }, + labels: [], + body: "Validation: npm test", + }); + await env.DB.prepare("update pull_requests set author_login = null where repo_full_name = ? and number = ?").bind("JSONbored/gittensory", 48).run(); + const checkedPanel = [ + "", + "", + "- [x] Re-run Gittensory review", + ].join("\n"); + vi.stubGlobal("fetch", async () => new Response("unexpected fetch", { status: 500 })); + + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-retrigger-unknown-actor", + eventName: "issue_comment", + payload: { + action: "edited", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 48, title: "Unknown panel refresh actor", state: "open", pull_request: {} }, + comment: { id: 780, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + }, + }); + + const audit = await env.DB.prepare("select actor, target_key, detail from audit_events where event_type = ?") + .bind("github_app.pr_panel_retrigger_skipped") + .first<{ actor: string | null; target_key: string; detail: string }>(); + expect(audit).toMatchObject({ + actor: null, + target_key: "JSONbored/gittensory#48", + detail: "not_maintainer_or_pr_author", + }); + }); + + it("ignores invalid rerun task edits and audits skipped rerun requests", async () => { + const env = createTestEnv(); + const checkedPanel = [ + "", + "", + "- [x] Re-run Gittensory review", + ].join("\n"); + const uncheckedPanel = checkedPanel.replace("- [x]", "- [ ]"); + let fetchCalls = 0; + vi.stubGlobal("fetch", async () => { + fetchCalls += 1; + return new Response("unexpected fetch", { status: 500 }); + }); + const basePayload = { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 46, title: "Panel skip", state: "open", user: { login: "contributor" }, pull_request: {} }, + }; + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", {}); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-created-ignore", + eventName: "issue_comment", + payload: { + action: "created", + ...basePayload, + comment: { id: 800, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-unchecked-ignore", + eventName: "issue_comment", + payload: { + action: "edited", + ...basePayload, + comment: { id: 801, body: uncheckedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-non-bot-ignore", + eventName: "issue_comment", + payload: { + action: "edited", + ...basePayload, + comment: { id: 802, body: checkedPanel, user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-missing-comment-ignore", + eventName: "issue_comment", + payload: { action: "edited", ...basePayload, sender: { login: "maintainer", type: "User" } }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-missing-panel-marker-ignore", + eventName: "issue_comment", + payload: { + action: "edited", + ...basePayload, + comment: { id: 806, body: "- [x] Re-run Gittensory review", user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-missing-rerun-marker-ignore", + eventName: "issue_comment", + payload: { + action: "edited", + ...basePayload, + comment: { id: 807, body: "\n\n- [x] Re-run Gittensory review", user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-other-bot-ignore", + eventName: "issue_comment", + payload: { + action: "edited", + ...basePayload, + comment: { id: 808, body: checkedPanel, user: { login: "other[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-bot-skip", + eventName: "issue_comment", + payload: { + action: "edited", + ...basePayload, + comment: { id: 803, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "gittensory[bot]", type: "Bot" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-missing-cache", + eventName: "issue_comment", + payload: { + action: "edited", + ...basePayload, + comment: { id: 804, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "panel-rerun-missing-context", + eventName: "issue_comment", + payload: { + action: "edited", + comment: { id: 805, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(fetchCalls).toBe(0); + const skips = await env.DB.prepare("select detail from audit_events where event_type = ? order by detail") + .bind("github_app.pr_panel_retrigger_skipped") + .all<{ detail: string }>(); + expect(skips.results.map((event) => event.detail)).toEqual(["bot_author", "cached_pr_missing", "missing_repo_pr_or_installation"]); + }); + + it("debounces noisy PR events without publishing public surfaces", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_and_label", + autoLabelEnabled: true, + checkRunMode: "enabled", + gateCheckMode: "enabled", reviewCheckMode: "required", + }); + let publicCalls = 0; + vi.stubGlobal("fetch", async () => { + publicCalls += 1; + return new Response("unexpected public call", { status: 500 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", {}); + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-labeled-noisy", + eventName: "pull_request", + payload: { + action: "labeled", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 44, title: "Noisy event PR", state: "open", user: { login: "contributor" }, head: { sha: "noisy123" }, labels: [{ name: "bug" }], body: "Fixes #1" }, + }, + }); + + expect(publicCalls).toBe(0); + }); + + it("processes GitHub webhook jobs for PRs, issues, comments-off, comment-attempt, and deleted installs", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 1, + title: "Prior merged work", + state: "closed", + merged_at: "2026-05-01T00:00:00.000Z", + user: { login: "oktofeesh1" }, + labels: [{ name: "bug" }], + body: "Fixes #1", + }); + const visibleCalls = { comments: 0, labelsCreated: 0, labelsApplied: 0, checks: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; + expect(body.body).toContain(""); + expect(body.body).toContain("Confirmed Gittensor contributor"); + expect(body.body).not.toMatch(/reviewability|likely_duplicate|reward|scoreability|estimated score|wallet|hotkey|trust score|payout|farming/i); + visibleCalls.comments += 1; + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + if (url.includes("/issues/3/labels") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/labels") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { labels?: string[] }; + expect(body.labels).toEqual(["gittensor"]); + visibleCalls.labelsApplied += 1; + return Response.json([{ name: "gittensor" }]); + } + if (url.includes("/repos/JSONbored/gittensory/labels") && !url.includes("/issues/") && method === "GET") return Response.json([]); + if (url.includes("/repos/JSONbored/gittensory/labels") && !url.includes("/issues/") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string }; + expect(body.name).toBe("gittensor"); + visibleCalls.labelsCreated += 1; + return Response.json({ name: "gittensor" }, { status: 201 }); + } + if (url.includes("/check-runs")) { + visibleCalls.checks += 1; + return new Response("checks disabled", { status: 500 }); + } + return new Response("not found", { status: 404 }); + }); + + const basePayload = { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + }; + + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSignalLevel: "standard", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + backfillEnabled: true, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-off", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + pull_request: { + number: 2, + title: "Fix webhook duplicate delivery", + state: "open", + user: { login: "oktofeesh1" }, + labels: [{ name: "bug" }], + body: "Fixes #1", + }, + }, + }); + expect(await listPullRequests(env, "JSONbored/gittensory")).toEqual(expect.arrayContaining([expect.objectContaining({ number: 2 })])); + + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: true, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + backfillEnabled: true, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-comment-attempt", + eventName: "pull_request", + payload: { + action: "synchronize", + ...basePayload, + pull_request: { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + labels: [{ name: "bug" }], + body: "Fixes #1", + }, + }, + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-comment-undetected", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + pull_request: { + number: 4, + title: "New contributor work", + state: "open", + user: { login: "newbie" }, + labels: [], + body: "Fixes #1", + }, + }, + }); + + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "minimal", + publicSurface: "comment_and_label", + autoLabelEnabled: true, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + backfillEnabled: true, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-comment-no-author", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + pull_request: { + number: 5, + title: "Anonymous webhook work", + state: "open", + labels: [], + body: "Fixes #1", + }, + }, + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "issue", + eventName: "issues", + payload: { + action: "opened", + ...basePayload, + issue: { + number: 1, + title: "Webhook duplicate delivery", + state: "open", + user: { login: "reporter" }, + labels: [{ name: "bug" }], + body: "Duplicate delivery should be idempotent.", + }, + }, + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "deleted", + eventName: "installation", + payload: { action: "deleted", installation: { id: 123 } }, + }); + + expect(visibleCalls).toEqual({ comments: 1, labelsCreated: 1, labelsApplied: 1, checks: 0 }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ? order by created_at").bind("github_app.pr_visibility_skipped").all<{ + detail: string; + }>(); + expect(skipped.results.map((event) => event.detail)).toEqual(expect.arrayContaining(["not_official_gittensor_miner", "missing_author"])); + }); + + // #1007 convergence (Stage D): with GITTENSORY_REVIEW_UNIFIED_COMMENT on AND the gate evaluating, the public PR-panel + // comment is rendered by the UNIFIED renderer (GitHub alert + synthesized "Code review" row) instead of the + // legacy panel — while STILL leading with the same panel marker so the in-place upsert updates the same + // comment. Mirrors the legacy panel-posting setup (confirmed miner + comment_and_label) but flips the flag + // and enables the gate so `maybePublishPrPublicSurface` takes the flag-ON branch. + it("renders the unified PR-review comment when the flag is on and the gate evaluates", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + }); + let postedBody = ""; + const calls = { comments: 0, gateChecks: 0 }; + let gateFinalized = false; + let failedPostGateMint = false; + const liveCiSpy = vi + .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") + .mockRejectedValueOnce(new Error("transient CI read failed")) + .mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) { + if (gateFinalized && !failedPostGateMint) { + failedPostGateMint = true; + return new Response("mint failed", { status: 500 }); + } + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + // PR files — the unified branch (re)fetches them to count changed files for the readiness chip. + if (url.includes("/pulls/3/files")) return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); + // #review-audit: the LIVE merge-state the comment now reads — the base just advanced with a conflict, so the + // live state is `dirty` even though the stored mergeableState (unset on this payload) would not say so. + if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "dirty" }); + // Gate check-run — must succeed so `gateEvaluation` is produced and the flag-ON branch runs. + // The pending check is POSTed (in_progress), then PATCHed to its completed conclusion. + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + calls.gateChecks += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; + if (body.status !== "in_progress" || body.conclusion) { + gateFinalized = true; + clearInstallationTokenCacheForTest(); + } + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/check-runs/901") && method === "PATCH") { + calls.gateChecks += 1; + gateFinalized = true; + clearInstallationTokenCacheForTest(); + return Response.json({ id: 901 }); + } + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + calls.comments += 1; + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-unified-comment", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "unified123" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + const installationTokenCiReads = liveCiSpy.mock.calls.filter( + ([, , , token]) => token === "installation-token", + ); + expect(installationTokenCiReads).toHaveLength(2); + expect(calls.comments).toBe(2); + expect(failedPostGateMint).toBe(true); + // Still leads with the panel marker → the upsert updates the SAME sticky comment in place (no duplicate). + expect(postedBody).toContain(""); + // The UNIFIED shape, which the legacy body never emits: a full-comment GitHub alert wrapper… + expect(postedBody).toMatch(/> \[!(TIP|NOTE|WARNING|CAUTION)\]/); + // …and the renderer's synthesized "Code review" signal row (bold first table label). + expect(postedBody).toContain("**Code review**"); + // Public-safe by construction — no internal trust/economics fields leak through the unified renderer. + expect(postedBody).not.toMatch(/wallet|hotkey|reward|trust score/i); + // #review-audit (#4220): the comment reads the LIVE `dirty` merge-state (not the stale stored one), so it must + // NOT headline "safe to merge" while the disposition would auto-close the base-conflicting PR. + expect(postedBody).not.toMatch(/safe to merge/i); + // #1955: no `.gittensory.yml` was fetched here (the raw-content URL isn't stubbed, so it 404s and the + // manifest resolves to null) — review.effort_score is absent/default OFF, so the effort chip must NOT render. + expect(postedBody).not.toMatch(/review effort:/); + } finally { + liveCiSpy.mockRestore(); + } + }); + + it("INVARIANT (#4498): the disposition planner reuses the public surface's own live mergeable_state/CI read instead of re-fetching a third time", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + }); + let mergeableStateReads = 0; + // No mockRejectedValueOnce here -- unlike the "renders the unified PR-review comment" test above, every call + // succeeds identically, isolating the "both refreshes succeed" case this fix targets (a prior-call failure + // legitimately forces a genuine second live read, which is a different, already-covered scenario). + const liveCiSpy = vi.spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl").mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + // commentMode: "detected_contributors_only" requires the author to actually resolve as a detected + // Gittensor contributor for the unified-comment (and its live merge-state/CI refresh) code path to + // engage at all -- an empty miner match here would silently skip that whole block, same as the + // original "renders the unified PR-review comment" test's fixture this one is adapted from. + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { uid: 7, githubUsername: "oktofeesh1", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1, hotkey: "must-not-leak" }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + if (url.includes("/pulls/3/files")) return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); + if (/\/pulls\/3(?:\?|$)/.test(url) && method === "GET") { + mergeableStateReads += 1; + return Response.json({ number: 3, mergeable_state: "clean" }); + } + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-single-live-fetch", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Single live fetch per pass", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "singlefetch123" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + // 2, not 3: readiness's own cachedLiveMergeState/cachedLiveCiAggregate check contributes ONE legitimate, + // unrelated live read each (a genuine durable-cache miss on this never-before-seen head, unaffected by + // this fix), and maybePublishPrPublicSurface's own forced refresh contributes the other -- reused + // directly by the disposition planner instead of re-fetched a third time. Verified empirically: reverting + // this fix on this exact fixture produces 3 of each, confirming the fix removes exactly the redundant + // third call, not readiness's separate, necessary one. + expect(mergeableStateReads).toBe(2); + const installationTokenCiReads = liveCiSpy.mock.calls.filter(([, , , token]) => token === "installation-token"); + expect(installationTokenCiReads).toHaveLength(2); + } finally { + liveCiSpy.mockRestore(); + } + }); + + // #3609/#3610: same fixture as the unified-comment test above (screenshotsAllowed needs both the global flag + // AND the repo cutover allowlist — createTestEnv already defaults GITTENSORY_REVIEW_REPOS to include this + // repo), but the changed file is WEB-VISIBLE (isVisualPath) so the capture pipeline actually fires, proving + // resolveVisualCaptureConfig / buildCapture's config-threading (review.visual) is reached end to end from the + // real webhook path, not just from the pure-function unit tests in visual-capture.test.ts. + it("threads review.visual config into the capture pipeline and renders a Visual preview section (#3609 / #3610)", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", + GITTENSORY_REVIEW_SCREENSHOTS: "true", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + }); + let postedBody = ""; + const liveCiSpy = vi.spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl").mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) { + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + // A web-visible route file (isVisualPath) — the ONLY difference from the sibling unified-comment fixture — + // so screenshotsAllowed's file-touch gate opens and buildCapture actually runs for this PR. + if (url.includes("/pulls/3/files")) { + return Response.json([{ filename: "apps/gittensory-ui/src/routes/app.index.tsx", additions: 5, deletions: 1, status: "modified" }]); + } + if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + // Preview discovery (deployments / commit checks / PR comments): none configured for this fixture, so + // buildCapture's discovery chain finds nothing and falls back to placeholders — it's wrapped in its own + // try/catch, so a 404 here degrades to "no preview" rather than failing the capture or the review. + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-visual-config-wiring", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Update the app index route", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "visualcfg123" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + // The capture pipeline ran (resolveVisualCaptureConfig -> buildCapture, both reached only through this + // webhook path) and produced at least a placeholder-backed route, so the collapsible renders. + expect(postedBody).toContain("Visual preview"); + expect(postedBody).toContain("`/app`"); + // Public-safe by construction — no internal trust/economics fields leak through the shot URLs either. + expect(postedBody).not.toMatch(/wallet|hotkey|reward|trust score/i); + } finally { + liveCiSpy.mockRestore(); + } + }); + + // #4083: review.visual.enabled: false (config-as-code, VPS-only in practice) overrides the coarser + // GITTENSORY_REVIEW_SCREENSHOTS + GITTENSORY_REVIEW_REPOS env-var gate above — same fixture as the sibling + // test above (same webhook, same visual-file touch, same env flag ON), the ONLY difference being the + // .gittensory.yml content, so this isolates the new enabled:false branch in processors.ts. + it("skips the capture pipeline entirely when review.visual.enabled is false, even though the env-var gate allows it (#4083)", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", + GITTENSORY_REVIEW_SCREENSHOTS: "true", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + }); + let postedBody = ""; + const liveCiSpy = vi.spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl").mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n visual:\n enabled: false\n"); + } + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) { + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + if (url.includes("/pulls/3/files")) { + return Response.json([{ filename: "apps/gittensory-ui/src/routes/app.index.tsx", additions: 5, deletions: 1, status: "modified" }]); + } + if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); + if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-visual-config-disabled", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Update the app index route", + state: "open", + user: { login: "oktofeesh1" }, + // Empty sha + a present ref (the opposite combination from the sibling "threads review.visual config" + // test's { sha: "visualcfg123" }) so between the two tests, both branches of captureTarget's + // optional headSha/headRef spreads are exercised. + head: { sha: "", ref: "feature/visual-config-disabled" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + // review.visual.enabled: false overrode the env-var gate — no capture attempted, so no Visual preview + // section at all, even though the PR touches a visual file and GITTENSORY_REVIEW_SCREENSHOTS is on. + expect(postedBody).not.toContain("Visual preview"); + } finally { + liveCiSpy.mockRestore(); + } + }); + + // #1957: with the unified comment on AND `.gittensory.yml` opting into `review.changed_files_summary`, the + // rendered comment gains the deterministic "Changed files" collapsible built from the SAME PR-files fetch the + // unified branch already does for the readiness chip — no separate call, no AI. Mirrors the base unified-comment + // test above but adds the manifest opt-in and asserts the new section's presence + content. + it("renders the Changed files summary when review.changed_files_summary is on in .gittensory.yml", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + }); + let postedBody = ""; + const calls = { comments: 0, gateChecks: 0 }; + let gateFinalized = false; + let failedPostGateMint = false; + const liveCiSpy = vi + .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") + .mockRejectedValueOnce(new Error("transient CI read failed")) + .mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + // .gittensory.yml opts into the deterministic changed-files summary — no AI involved. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n changed_files_summary: true\n"); + } + if (url.includes("/access_tokens")) { + if (gateFinalized && !failedPostGateMint) { + failedPostGateMint = true; + return new Response("mint failed", { status: 500 }); + } + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + // PR files — the unified branch (re)fetches them to count changed files AND (with the toggle above) to + // build the "Changed files" summary. A doc + a source file so the summary shows 2 distinct category rows. + if (url.includes("/pulls/3/files")) + return Response.json([ + { filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }, + { filename: "README.md", additions: 2, deletions: 0, status: "modified" }, + ]); + if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); + // Gate check-run — must succeed so `gateEvaluation` is produced and the flag-ON branch runs. + // The pending check is POSTed (in_progress), then PATCHed to its completed conclusion. + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + calls.gateChecks += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; + if (body.status !== "in_progress" || body.conclusion) { + gateFinalized = true; + clearInstallationTokenCacheForTest(); + } + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/check-runs/901") && method === "PATCH") { + calls.gateChecks += 1; + gateFinalized = true; + clearInstallationTokenCacheForTest(); + return Response.json({ id: 901 }); + } + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + calls.comments += 1; + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-unified-comment-changed-files", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "unified456" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + expect(calls.comments).toBe(2); + expect(postedBody).toContain(""); + // The deterministic changed-files collapsible — per-file rows with GitHub Files-tab links (#2157). + expect(postedBody).toContain("Changed files"); + expect(postedBody).toContain("| `src/cache.ts` | +5 | -1 | [View diff](https://github.com/JSONbored/gittensory/pull/3/files#diff-"); + expect(postedBody).toContain("| `README.md` | +2 | -0 | [View diff](https://github.com/JSONbored/gittensory/pull/3/files#diff-"); + } finally { + liveCiSpy.mockRestore(); + } + }); + + // #1955: with the unified comment on AND `.gittensory.yml` opting into `review.effort_score`, the rendered + // comment gains the deterministic, no-AI "review effort: N/5 (~M min)" chip — computed by estimateReviewEffort + // from the SAME PR-files fetch the unified branch already does (no separate call). Mirrors the + // changed_files_summary test above but asserts the effort chip's presence + exact value instead. + it("renders the review effort chip when review.effort_score is on in .gittensory.yml", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + }); + let postedBody = ""; + const calls = { comments: 0, gateChecks: 0 }; + let gateFinalized = false; + let failedPostGateMint = false; + const liveCiSpy = vi + .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") + .mockRejectedValueOnce(new Error("transient CI read failed")) + .mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + // .gittensory.yml opts into the deterministic effort score — no AI involved. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n effort_score: true\n"); + } + if (url.includes("/access_tokens")) { + if (gateFinalized && !failedPostGateMint) { + failedPostGateMint = true; + return new Response("mint failed", { status: 500 }); + } + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + // PR files — the unified branch (re)fetches them to count changed files AND (with the toggle above) to + // compute the effort estimate. A 10-added-line source file WITH a patch (weighted 10) plus a docs file with + // NO `patch` field (exercises the `typeof file.payload?.patch === "string" ? ... : undefined` fallback -> + // addedLineCount(undefined) = 0, so it contributes 0 weighted lines but still its per-file overhead): + // weighted 10 + 0 + 2 files * 3 overhead = effort 16 -> band 2, minutes round(16 * 0.5) = 8 + // (see estimateReviewEffort — src/review/review-effort.ts). + if (url.includes("/pulls/3/files")) + return Response.json([ + { + filename: "src/cache.ts", + additions: 10, + deletions: 1, + status: "modified", + patch: `@@ -1,1 +1,11 @@\n${Array.from({ length: 10 }, (_, i) => `+const x${i} = ${i};`).join("\n")}`, + }, + { filename: "README.md", additions: 2, deletions: 0, status: "modified" }, + ]); + if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); + // Gate check-run — must succeed so `gateEvaluation` is produced and the flag-ON branch runs. + // The pending check is POSTed (in_progress), then PATCHed to its completed conclusion. + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + calls.gateChecks += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; + if (body.status !== "in_progress" || body.conclusion) { + gateFinalized = true; + clearInstallationTokenCacheForTest(); + } + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/check-runs/901") && method === "PATCH") { + calls.gateChecks += 1; + gateFinalized = true; + clearInstallationTokenCacheForTest(); + return Response.json({ id: 901 }); + } + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + calls.comments += 1; + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-unified-comment-effort-score", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "unified789" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + expect(calls.comments).toBe(2); + expect(postedBody).toContain(""); + // The new deterministic, no-AI chip: band 2 (effort 16 <= BAND_MAX[1]=40), minutes round(16*0.5)=8. + expect(postedBody).toContain("`review effort: 2/5 (~8 min)`"); + } finally { + liveCiSpy.mockRestore(); + } + }); + + // #2051/#4147: with the unified comment on AND `.gittensory.yml` opting into `review.auto_merge_summary`, + // the rendered comment gains the deterministic, no-AI "Auto-merge readiness" collapsible — computed from the + // SAME live CI state, gate conclusion, mergeable_state, and linked-issue facts this pass already resolves + // for the readiness chip and gate verdict, no extra fetch. Mirrors the effort_score test above but asserts + // the auto-merge-readiness table's presence + condition marks instead. + it("renders the Auto-merge readiness collapsible when review.auto_merge_summary is on in .gittensory.yml", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + }); + let postedBody = ""; + const calls = { comments: 0, gateChecks: 0 }; + let gateFinalized = false; + let failedPostGateMint = false; + const liveCiSpy = vi + .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") + .mockRejectedValueOnce(new Error("transient CI read failed")) + .mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + // .gittensory.yml opts into the deterministic auto-merge summary — no AI involved. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n auto_merge_summary: true\n"); + } + if (url.includes("/access_tokens")) { + if (gateFinalized && !failedPostGateMint) { + failedPostGateMint = true; + return new Response("mint failed", { status: 500 }); + } + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + if (url.includes("/pulls/3/files")) + return Response.json([{ filename: "src/cache.ts", additions: 10, deletions: 1, status: "modified" }]); + // mergeable_state: "clean" -> mergeableClean: true in the rendered table. + if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); + // Gate check-run — must succeed so `gateEvaluation` concludes "success" -> gatePassing: true. + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + calls.gateChecks += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; + if (body.status !== "in_progress" || body.conclusion) { + gateFinalized = true; + clearInstallationTokenCacheForTest(); + } + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/check-runs/901") && method === "PATCH") { + calls.gateChecks += 1; + gateFinalized = true; + clearInstallationTokenCacheForTest(); + return Response.json({ id: 901 }); + } + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + calls.comments += 1; + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-unified-comment-auto-merge-summary", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "unified789" }, + labels: [{ name: "bug" }], + // A linked issue (#1) is present -> linkedIssueValid: true in the rendered table. + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + expect(calls.comments).toBe(2); + expect(postedBody).toContain(""); + expect(postedBody).toContain("Auto-merge readiness"); + expect(postedBody).toContain("_Read-only snapshot of the current auto-merge conditions"); + // All four conditions pass with this fixture: CI green, gate passing, branch mergeable clean, valid + // linked issue. + expect(postedBody).toContain("| CI checks green | ✅ |"); + expect(postedBody).toContain("| Gate passing | ✅ |"); + expect(postedBody).toContain("| Branch mergeable (clean) | ✅ |"); + expect(postedBody).toContain("| Valid linked issue | ✅ |"); + } finally { + liveCiSpy.mockRestore(); + } + }); + + // #2044: `.gittensory.yml` `review.tone` is folded into the AI reviewer's system prompt by + // composeManifestReviewInstructions (src/signals/focus-manifest.ts), consumed by + // src/queue/processors.ts's aiReviewCacheReadDecideAndRun. That composition is unit-tested in isolation + // (focus-manifest.test.ts), but nothing previously drove the full webhook -> processJob -> runGittensoryAiReview + // pipeline to confirm the resolved tone text actually reaches env.AI.run's system message. Mirrors the + // changed_files_summary/effort_score tests above but captures the AI system prompt instead of the posted body. + it("threads review.tone from .gittensory.yml into the AI reviewer's system prompt (#2044)", async () => { + let capturedSystem = ""; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async (_model: string, options: { messages: Array<{ role: string; content: string }> }) => { + capturedSystem = options.messages[0]?.content ?? ""; + return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; + }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + // The repo's own review.tone opt-in (#2044) -- a maintainer voice brief, distinct from review.instructions. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n tone: Keep findings terse and skip pleasantries\n"); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "review-tone-system-prompt", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, + }, + }); + + // The composed tone section (composeManifestReviewInstructions) really reached env.AI.run's system message -- + // not just the pure-function assertion in focus-manifest.test.ts. + expect(capturedSystem).toContain( + "Review tone (maintainer voice brief — complements review.profile): Keep findings terse and skip pleasantries", + ); + }); + + // #review-exclude-paths / #2043: `review.exclude_paths`/`review.path_filters` are resolved by + // resolveReviewPromptOverrides and applied by filterReviewFilesForAi (src/signals/focus-manifest.ts), consumed + // by src/queue/processors.ts's runAiReviewForAdvisory -- but ONLY in advisory mode (block mode intentionally + // reviews the full diff so a filtered path can never bypass an AI consensus blocker). filterReviewFilesForAi + // itself is unit-tested as a pure function (focus-manifest.test.ts); every existing e2e assertion of this field + // elsewhere in this file only ever passes EMPTY excludePaths/pathFilters arrays (cache-fingerprint checks), so + // nothing previously proved a NON-EMPTY glob genuinely removes a matching file from what the AI reviewer sees. + it("genuinely removes a review.exclude_paths match from the AI reviewer's diff in advisory mode (#review-exclude-paths)", async () => { + let capturedUser = ""; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { + run: async (_model: string, options: { messages: Array<{ role: string; content: string }> }) => { + capturedUser = options.messages[1]?.content ?? ""; + return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; + }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + // advisory (NOT block): block mode always reviews the full diff, ignoring exclude_paths/path_filters, so + // only advisory mode exercises the filterReviewFilesForAi branch (src/queue/processors.ts). + aiReviewMode: "advisory", + // The PR author below is an unconfirmed contributor; aiReviewAllAuthors is the documented per-repo opt-in + // that widens the AI-spend gate to every author (already unit-tested in ai-review-advisory.test.ts) so this + // test doesn't also have to stand up the full miner-confirmation registry mocks just to reach the AI call. + aiReviewAllAuthors: true, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) + return Response.json([ + { filename: "src/real-change.ts", status: "modified", additions: 3, deletions: 0, patch: "@@ -1,1 +1,4 @@\n+export const real = 1;\n+export const two = 2;\n+export const three = 3;" }, + { filename: "src/schema.generated.ts", status: "modified", additions: 1, deletions: 0, patch: "@@ -1,1 +1,2 @@\n+export const generatedMarker = true;" }, + ]); + if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + // The repo's own review.exclude_paths opt-in -- a NON-EMPTY glob (#review-exclude-paths), unlike every + // existing fingerprint-only assertion of this field elsewhere in this file. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response('review:\n exclude_paths:\n - "**/*.generated.ts"\n'); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "review-exclude-paths-ai-diff", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, + }, + }); + + // The non-excluded file's diff genuinely reached the AI reviewer's user prompt... + expect(capturedUser).toContain("src/real-change.ts"); + // ...but the exclude_paths match is genuinely ABSENT -- not merely uncounted -- from what the AI reviewer + // sees: neither its path nor its patch content leaked into the prompt. + expect(capturedUser).not.toContain("schema.generated.ts"); + expect(capturedUser).not.toContain("generatedMarker"); + }); + + // #2049: with the unified comment on AND `.gittensory.yml` setting `review.max_findings`, the processor wires + // manifest caps into `buildUnifiedCommentBody` and the renderer truncates blocker/nit lists with a "+N more" + // footer. Mirrors the effort_score test above but asserts display-only truncation instead. + it("truncates unified-comment blockers when review.max_findings is set in .gittensory.yml (#2049)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + linkedIssueGateMode: "block", + }); + let postedBody = ""; + const calls = { comments: 0, gateChecks: 0 }; + let gateFinalized = false; + let failedPostGateMint = false; + const liveCiSpy = vi + .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") + .mockRejectedValueOnce(new Error("transient CI read failed")) + .mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n max_findings:\n blockers: 0\n"); + } + if (url.includes("/access_tokens")) { + if (gateFinalized && !failedPostGateMint) { + failedPostGateMint = true; + return new Response("mint failed", { status: 500 }); + } + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + if (url.includes("/pulls/3/files")) + return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); + if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + calls.gateChecks += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; + if (body.status !== "in_progress" || body.conclusion) { + gateFinalized = true; + clearInstallationTokenCacheForTest(); + } + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/check-runs/901") && method === "PATCH") { + calls.gateChecks += 1; + gateFinalized = true; + clearInstallationTokenCacheForTest(); + return Response.json({ id: 901 }); + } + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + calls.comments += 1; + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-unified-comment-max-findings", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "unifiedmaxfindings" }, + labels: [{ name: "bug" }], + body: "No linked issue on purpose.\n\nValidation: npm test", + }, + }, + }); + + expect(calls.comments).toBe(2); + expect(postedBody).toContain(""); + expect(postedBody).toContain("_+1 more_"); + } finally { + liveCiSpy.mockRestore(); + } + }); + + // #2181 (apply slice of #1964): review.memory end-to-end through the real webhook path. A `qualityGateMode: + // "advisory"` + an unreachable `qualityGateMinScore: 100` deterministically produces the + // `readiness_score_below_threshold` ADVISORY (never a blocker — readiness stays advisory-only, see + // rules.test.ts) warning finding on every pass, giving a stable target to record a suppression signal against + // and verify it is (or is not) suppressed from the rendered unified comment. The manifest is seeded DIRECTLY + // via upsertRepoFocusManifest (bypassing the 6h .gittensory.yml fetch cache) so each test's `.gittensory.yml` + // fetch response is never actually needed on the hot path — it only serves as an inert 404 fallback. + async function runReadinessWarningPass(env: Env, opts: { deliveryId: string; headSha: string; reviewMemoryManifest: boolean }) { + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + qualityGateMode: "advisory", + qualityGateMinScore: 100, + }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", opts.reviewMemoryManifest ? { review: { memory: true } } : {}); + let postedBody = ""; + let gateFinalized = false; + let failedPostGateMint = false; + const liveCiSpy = vi + .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") + .mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("not found", { status: 404 }); + } + if (url.includes("/access_tokens")) { + if (gateFinalized && !failedPostGateMint) { + failedPostGateMint = true; + return new Response("mint failed", { status: 500 }); + } + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + if (url.includes("/pulls/3/files")) + return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); + if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; + if (body.status !== "in_progress" || body.conclusion) { + gateFinalized = true; + clearInstallationTokenCacheForTest(); + } + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/check-runs/901") && method === "PATCH") { + gateFinalized = true; + clearInstallationTokenCacheForTest(); + return Response.json({ id: 901 }); + } + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + try { + await processJob(env, { + type: "github-webhook", + deliveryId: opts.deliveryId, + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: opts.headSha }, + labels: [{ name: "bug" }], + // No linked issue AND no validation evidence -- keeps the readiness score comfortably below the + // unreachable qualityGateMinScore: 100 threshold above, so readiness_score_below_threshold fires + // deterministically regardless of the panel's exact scoring breakdown. + body: "No linked issue, no validation evidence on purpose.", + }, + }, + }); + } finally { + liveCiSpy.mockRestore(); + } + return postedBody; + } + + it("FLAG-OFF (default): review.memory in .gittensory.yml alone never suppresses the readiness warning (operator kill-switch required)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + // review.memory: true in the manifest, but NO GITTENSORY_REVIEW_MEMORY env flag on this env -- byte-identical. + const postedBody = await runReadinessWarningPass(env, { + deliveryId: "review-memory-flag-off", + headSha: "revmem-flag-off", + reviewMemoryManifest: true, + }); + expect(postedBody).toContain("Readiness score is below the configured threshold"); + }); + + it("FLAG-ON: suppresses a readiness warning EXACTLY matching a previously recorded suppression signal", async () => { + const seedEnv = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + // A throwaway pass (flag/manifest both off — byte-identical review path) against a SEPARATE, disposable D1 + // instance just to learn the finding's REAL, LIVE-computed readiness score (a pure function of the fixed + // PR/settings fixture above, so it reproduces identically for the real pass below on its own fresh `env`). + // The rendered nit itself only carries `title`+`action` (see buildDualReviewNotes's gateNits) — the score + // comes from the status chip. + const seedBody = await runReadinessWarningPass(seedEnv, { deliveryId: "review-memory-seed", headSha: "revmem-seed", reviewMemoryManifest: false }); + expect(seedBody).toContain("Readiness score is below the configured threshold"); + const scoreMatch = /readiness (\d+)\/100/.exec(seedBody); + expect(scoreMatch).not.toBeNull(); + const score = Number(scoreMatch![1]); + // Reconstructs buildQualityGateWarning's exact title+detail template (src/rules/advisory.ts) from the live + // score + the qualityGateMinScore: 100 configured above, so the computed patternHash matches the real finding. + const detail = `The public readiness score is ${score}/100, below the repository threshold of 100/100.`; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", GITTENSORY_REVIEW_MEMORY: "true" }); + await recordReviewSuppression(env, { + repoFullName: "JSONbored/gittensory", + category: "readiness_score_below_threshold", + patternHash: reviewMemoryFingerprint({ + category: "readiness_score_below_threshold", + message: `Readiness score is below the configured threshold ${detail}`, + }), + createdBy: "maintainer1", + }); + // The flag is ON (env + manifest) and the exact-match signal is now stored -- the warning must be + // suppressed from the rendered unified comment. + const postedBody = await runReadinessWarningPass(env, { + deliveryId: "review-memory-flag-on", + headSha: "revmem-flag-on", + reviewMemoryManifest: true, + }); + expect(postedBody).not.toContain("Readiness score is below the configured threshold"); + }); + + it("FLAG-ON, no stored signals: neither suppresses nor demotes -- the warning renders exactly as if review.memory were off (REGRESSION: the all-clear branch where the store read succeeds but finds nothing to apply)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", GITTENSORY_REVIEW_MEMORY: "true" }); + // Flag is fully ON (env + manifest) and the suppression-store read succeeds, but NO signal has ever been + // recorded for this repo -- applyReviewMemorySuppression's own empty-signals short-circuit returns + // suppressedCount: 0, demotedCount: 0, so processors.ts's "anything to apply?" check is false and + // renderedGate is never reassigned away from the original commentGate. + const postedBody = await runReadinessWarningPass(env, { + deliveryId: "review-memory-no-signals", + headSha: "revmem-no-signals", + reviewMemoryManifest: true, + }); + expect(postedBody).toContain("Readiness score is below the configured threshold"); + }); + + it("FLAG-ON: DEMOTES (keeps, but does not suppress) a same-category readiness warning that does not exactly match any stored signal", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", GITTENSORY_REVIEW_MEMORY: "true" }); + // A signal for the SAME category but a patternHash that can never match this PR's real finding -- exercises + // the "demote" (scope-matched, hash-mismatched) branch instead of "suppress". + await recordReviewSuppression(env, { + repoFullName: "JSONbored/gittensory", + category: "readiness_score_below_threshold", + patternHash: "never-matches-the-real-finding", + createdBy: "maintainer1", + }); + const postedBody = await runReadinessWarningPass(env, { + deliveryId: "review-memory-demote", + headSha: "revmem-demote", + reviewMemoryManifest: true, + }); + // Demoted (not suppressed) -- the finding still renders in the comment. + expect(postedBody).toContain("Readiness score is below the configured threshold"); + }); + + it("FLAG-ON, fail-safe: a suppression-store read error leaves the readiness warning untouched rather than throwing", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", GITTENSORY_REVIEW_MEMORY: "true" }); + const listSpy = vi.spyOn(repositoriesModule, "listReviewSuppressions").mockRejectedValue(new Error("D1 unavailable")); + try { + const postedBody = await runReadinessWarningPass(env, { + deliveryId: "review-memory-store-error", + headSha: "revmem-store-error", + reviewMemoryManifest: true, + }); + expect(postedBody).toContain("Readiness score is below the configured threshold"); + } finally { + listSpy.mockRestore(); + } + }); + + // #1955: the review-effort minutes persisted onto the public-stats audit event (independent of + // review.effort_score, which only gates the unified-comment CHIP) must never block the publish itself when the + // estimator throws — the publish still completes and simply omits `reviewEffortMinutes` from the event metadata + // (public-stats.ts's own COALESCE-style fallback then applies, same as a pre-#1955 historical row). + it("swallows an estimateReviewEffort failure when persisting the public-stats minutes — the publish still completes", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "off", gatePack: "oss-anti-slop" }); + const estimateSpy = vi.spyOn(reviewEffortModule, "estimateReviewEffort").mockImplementationOnce(() => { + throw new Error("estimator blew up"); + }); + let commentPosted = false; + let publishedMetadata: Record | undefined; + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.pr_public_surface_published") { + publishedMetadata = event.metadata as Record; + } + await originalRecordAuditEvent(auditEnv, event); + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/8/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/8")) return Response.json({ number: 8, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }); + if (url.includes("/commits/a8/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a8/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/8/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/8/comments") && method === "POST") { commentPosted = true; return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + try { + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "effort-estimator-throws", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 8, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }, + }, + }), + ).resolves.toBeUndefined(); + + expect(commentPosted).toBe(true); // the publish completed despite the estimator throwing + expect(estimateSpy).toHaveBeenCalled(); + expect(publishedMetadata).toBeDefined(); + expect(publishedMetadata).not.toHaveProperty("reviewEffortMinutes"); + } finally { + estimateSpy.mockRestore(); + auditSpy.mockRestore(); + } + }); + + // #1958: with inline comments AND finding categories both on in .gittensory.yml (finding_categories rides on + // inline_comments, exactly like suggestions did for #1956), the model is asked to self-categorize each + // inlineFindings item, and BOTH surfaces render it — the posted inline review comment label AND the unified + // comment's new "Finding categories" collapsible. + it("renders finding categories in the inline comment label and the unified comment's Finding categories section when review.finding_categories is on", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", + GITTENSORY_REVIEW_INLINE_COMMENTS: "true", + GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", + AI: { + run: async () => + ({ + response: JSON.stringify({ + assessment: "Looks fine overall.", + blockers: [], + nits: [], + suggestions: [], + inlineFindings: [ + { path: "src/db.ts", line: 2, severity: "nit", body: "This query is vulnerable to SQL injection.", category: "security" }, + ], + }), + }) as { response: string }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + aiReviewMode: "block", + gatePack: "oss-anti-slop", + }); + let inlineReviewComments: Array<{ body: string }> = []; + let unifiedCommentBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // .gittensory.yml opts into inline comments AND finding categories together. + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + return new Response("review:\n inline_comments: true\n finding_categories: true\n"); + } + if (url.includes("/pulls/8/files")) + return Response.json([{ filename: "src/db.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@ -1,1 +1,2 @@\n ctx\n+export const ok = true;" }]); + if (url.endsWith("/pulls/8")) return Response.json({ number: 8, title: "Add query helper", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a8/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a8/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + // The separate, quiet inline-review post (event: COMMENT) — distinct from the sticky unified issue comment. + if (url.endsWith("/pulls/8/reviews") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { comments?: Array<{ body: string }> }; + inlineReviewComments = body.comments ?? []; + return Response.json({ id: 55 }); + } + if (url.includes("/issues/8/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/8/comments") && method === "POST") { + unifiedCommentBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1 }, { status: 201 }); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-finding-categories", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 8, title: "Add query helper", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }, + }, + }); + + // The inline PR-review comment label carries the category tag. + expect(inlineReviewComments[0]?.body).toBe("**Nit · Security:** This query is vulnerable to SQL injection."); + // The unified comment's new collapsible counts it too. + expect(unifiedCommentBody).toContain("Finding categories"); + expect(unifiedCommentBody).toContain("| Security | 1 |"); + }); + + // #1971: a FROZEN (manual-review) PR reuses its last published AI review, which carries no impact-map entries — + // the unified comment still renders, and the impact-map render arm degrades to no section (aiReview present but + // aiReview.impactMap undefined ⇒ `aiReview?.impactMap ?? []` ⇒ [] ⇒ buildImpactMapCollapsible null). + it("renders the unified comment WITHOUT an Impact map section when a frozen review is reused (no threaded entries)", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Fresh.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await persistRegistrySnapshot(env, normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z")); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "block", gatePack: "oss-anti-slop" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 77, title: "Held PR", state: "open", user: { login: "contributor" }, head: { sha: "a77" }, labels: [{ name: "manual-review" }], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 77, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + // A prior PUBLISHED review for this exact head — the freeze path reuses it (aiReview = frozenReview) instead of + // spending a fresh AI call. Its cached shape has notes+reviewerCount but NO impactMap, so the render arm's + // nullish arm fires. + await putCachedAiReview(env, "JSONbored/gittensory", 77, "a77", "block", { notes: "Prior published review.", reviewerCount: 1 }); + await markAiReviewPublished(env, "JSONbored/gittensory", 77, "a77"); + let unifiedCommentBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/77/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.endsWith("/pulls/77")) return Response.json({ number: 77, title: "Held PR", state: "open", user: { login: "contributor" }, head: { sha: "a77" }, labels: [{ name: "manual-review" }], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a77/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a77/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/issues/77/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/77/comments")) { unifiedCommentBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? unifiedCommentBody); return Response.json({ id: 1 }, { status: 201 }); } + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "impact-map-frozen-reuse", repoFullName: "JSONbored/gittensory", prNumber: 77, installationId: 123 }); + + expect(aiCalls).toBe(0); // frozen ⇒ reused, no fresh AI + expect(unifiedCommentBody).toContain("gittensory-pr-panel"); // the unified panel rendered from the frozen review + expect(unifiedCommentBody).not.toContain("Impact map"); // ...with no impact-map section (reused review has none) + }); + + // #1962: with BOTH the operator flag and the manifest opt-in on, the review emits a "Fix handoff" collapsible — + // one machine-readable block per inline finding a contributor's own local agent can consume — in the unified + // comment. Flag-OFF (every other review test) ⇒ no such section. + it("emits the Fix handoff collapsible in the unified comment when review.fixHandoff + the operator flag are on", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", + GITTENSORY_REVIEW_INLINE_COMMENTS: "true", + GITTENSORY_REVIEW_FIX_HANDOFF: "true", + GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", + AI: { + run: async () => + ({ + response: JSON.stringify({ + assessment: "One real issue.", + blockers: [], + nits: [], + suggestions: [], + inlineFindings: [ + { path: "src/db.ts", line: 2, severity: "blocker", body: "This query is vulnerable to SQL injection.", suggestion: "Use a parameterized query." }, + ], + }), + }) as { response: string }, + } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "block", gatePack: "oss-anti-slop" }); + let unifiedCommentBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { + // NOTE: the manifest key is camelCase `fixHandoff` (unlike snake-case `finding_categories`) — see focus-manifest parse. + return new Response("review:\n inline_comments: true\n fixHandoff: true\n"); + } + if (url.includes("/pulls/9/files")) + return Response.json([{ filename: "src/db.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@ -1,1 +1,2 @@\n ctx\n+export const ok = true;" }]); + if (url.endsWith("/pulls/9")) return Response.json({ number: 9, title: "Add query helper", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a9/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a9/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + if (url.endsWith("/pulls/9/reviews") && method === "POST") return Response.json({ id: 55 }); + if (url.includes("/issues/9/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/9/comments") && method === "POST") { + unifiedCommentBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1 }, { status: 201 }); + } + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-fix-handoff", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 9, title: "Add query helper", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "Closes #1" }, + }, + }); + + expect(unifiedCommentBody).toContain("Fix handoff"); // the collapsible section is emitted + expect(unifiedCommentBody).toContain("Fix handoff — Blocker at `src/db.ts:2`"); // the per-finding block header + location anchor + expect(unifiedCommentBody).toContain("This query is vulnerable to SQL injection."); // the finding, handed off verbatim + expect(unifiedCommentBody).toContain("Suggested change:"); // its suggestion carried through + }); + + // FIX B + FIX D3 at the processor call site: a unified comment for a PR whose CI has a FAILED check, with the + // PR's files only available from GitHub (stored rows empty) — proves (B) the inline file fetch populates the + // real diff/changed-file count on the first review, and (D3) the failing check name + its per-check WHY render + // under a "CI checks failing" section (not just a bare "CI failing" chip). + it("inline-fetches the PR files and renders failing CI check names + reasons in the unified comment (FIX B + D3)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITHUB_PUBLIC_TOKEN: "public-token", GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + }); + // Seed a FAILED check summary with a per-check WHY (codecov-style) so listCheckSummaries returns it and the + // unified site populates failingDetails. (The PR row + headSha must match for the check to associate.) + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "unified123" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }); + await upsertCheckSummary(env, { + id: "JSONbored/gittensory#unified123#codecov/patch", + repoFullName: "JSONbored/gittensory", + pullNumber: 3, + headSha: "unified123", + name: "codecov/patch", + status: "completed", + conclusion: "failure", + detailsUrl: "https://codecov.io/report", + payload: { output: { summary: "60% of diff hit (target 97%)" } }, + }); + let postedBody = ""; + let filesFetched = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // FIX B: stored pull_request_files is empty, so the review path inline-fetches from GitHub here. + if (url.includes("/pulls/3/files")) { + filesFetched += 1; + return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified", patch: "@@\n+const x = 1;" }]); + } + // The review path now reads the LIVE CI aggregate (check-runs + commit-statuses). codecov/patch is a + // classic COMMIT-STATUS (not a check-run), so it comes from the combined-status endpoint; the check-runs + // list stays empty (it must, so the gate's own check-run upsert finds no pre-existing run to PATCH). + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/") && url.includes("/status")) + return Response.json({ state: "failure", statuses: [{ context: "codecov/patch", state: "failure", description: "60% of diff hit (target 97%)", target_url: "https://codecov.io/report" }] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 902 }, { status: 201 }); + if (url.includes("/check-runs/902") && method === "PATCH") return Response.json({ id: 902 }); + if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/3/comments") && method === "POST") { + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-unified-ci-failing", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 3, + title: "Fix webhook duplicate delivery again", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "unified123" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + // FIX B: the files were fetched inline from GitHub (stored rows were empty) and the changed-file count is real. + expect(filesFetched).toBeGreaterThan(0); + expect(postedBody).toContain("`1 file`"); + // FIX D3: the failing check name + its WHY render under a "CI checks failing" section, plus the chip. + expect(postedBody).toContain("`CI failing`"); + expect(postedBody).toContain("CI checks failing"); + expect(postedBody).toContain("codecov/patch"); + expect(postedBody).toContain("60% of diff hit (target 97%)"); + // Still public-safe. + expect(postedBody).not.toMatch(/wallet|hotkey|reward|trust score/i); + }); + + // REGRESSION (#4414-class advisory holds): a third-party app's COMPLETED action_required check-run that is + // NOT a branch-protection required context (e.g. Superagent's "Contributor trust", posted alongside its own + // separate, actually-required "Superagent Security Scan") must never flip ciState to "failed" or post under + // "CI checks failing" -- that auto-closes real contributor PRs (#4414's regression). It must still be VISIBLE, + // under its own non-blocking "Flagged checks" section, so a maintainer can act on it without the PR being + // silently waved through OR silently closed. + it("REGRESSION (#4414-class advisory holds): a non-required third-party action_required check renders as a non-blocking 'Flagged checks' note, not a CI failure", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITHUB_PUBLIC_TOKEN: "public-token", GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 6, + title: "Fix flaky retry test", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "flagged456" }, + base: { ref: "main" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/6/files")) return Response.json([{ filename: "src/retry.ts", additions: 3, deletions: 1, status: "modified", patch: "@@\n+const x = 1;" }]); + // Branch protection requires ONLY "validate" + "Superagent Security Scan" -- NOT "Contributor trust", + // matching the real-world JSONbored/gittensory config that #4414 broke. + if (url.includes("/branches/main/protection/required_status_checks")) return Response.json({ contexts: ["validate", "Superagent Security Scan"] }); + if (url.includes("/check-runs") && method === "GET") { + return Response.json({ + total_count: 3, + check_runs: [ + { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "Superagent Security Scan", status: "completed", conclusion: "success", app: { slug: "superagent-security" } }, + { + name: "Contributor trust", + status: "completed", + conclusion: "action_required", + app: { slug: "superagent-security" }, + output: { title: "Manual review needed" }, + details_url: "https://superagent.example/checks/contributor-trust", + }, + ], + }); + } + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 903 }, { status: 201 }); + if (url.includes("/check-runs/903") && method === "PATCH") return Response.json({ id: 903 }); + if (url.includes("/issues/6/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/6/comments") && method === "POST") { + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-flagged-nonrequired-check", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 6, + title: "Fix flaky retry test", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "flagged456" }, + base: { ref: "main" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + // Never a CI failure -- the non-required check must not flip ciState/block the PR. + expect(postedBody).not.toContain("`CI failing`"); + expect(postedBody).not.toContain("CI checks failing"); + // But never silently invisible either -- surfaced as its own non-blocking note, with its per-check WHY. + expect(postedBody).toContain("Flagged checks (non-blocking)"); + expect(postedBody).toContain("Contributor trust"); + expect(postedBody).toContain("Manual review needed"); + expect(postedBody).not.toMatch(/wallet|hotkey|reward|trust score/i); + }); + + it("REGRESSION (#4414-class advisory holds): a bare non-required action_required check (no output/details_url) still renders under 'Flagged checks', name-only", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITHUB_PUBLIC_TOKEN: "public-token", GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 7, + title: "Bump lockfile", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "flagged457" }, + base: { ref: "main" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { uid: 7, githubUsername: "oktofeesh1", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1, hotkey: "must-not-leak" }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/7/files")) return Response.json([{ filename: "package-lock.json", additions: 2, deletions: 2, status: "modified", patch: "@@\n+1" }]); + if (url.includes("/branches/main/protection/required_status_checks")) return Response.json({ contexts: ["validate", "Superagent Security Scan"] }); + if (url.includes("/check-runs") && method === "GET") { + return Response.json({ + total_count: 3, + check_runs: [ + { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, + { name: "Superagent Security Scan", status: "completed", conclusion: "success", app: { slug: "superagent-security" } }, + // Bare: no output, no details_url -- the common real-world shape for a check-run with nothing to say. + { name: "Contributor trust", status: "completed", conclusion: "action_required", app: { slug: "superagent-security" } }, + ], + }); + } + if (url.includes("/commits/") && url.includes("/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 904 }, { status: 201 }); + if (url.includes("/check-runs/904") && method === "PATCH") return Response.json({ id: 904 }); + if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/7/comments") && method === "POST") { + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-flagged-nonrequired-check-bare", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 7, + title: "Bump lockfile", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "flagged457" }, + base: { ref: "main" }, + labels: [{ name: "bug" }], + body: "Fixes #1\n\nValidation: npm test", + }, + }, + }); + + expect(postedBody).not.toContain("CI checks failing"); + expect(postedBody).toContain("Flagged checks (non-blocking)"); + expect(postedBody).toContain("- Contributor trust"); + }); + + it("skips bots and maintainer authors, and keeps explicitly enabled checks minimal", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + }); + const calls = { minerList: 0, checks: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", hotkey: "must-not-cache", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/abc123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { output?: { title?: string; text?: string } }; + expect(body.output?.text).toBe("No detailed findings are published in check runs."); + calls.checks += 1; + return Response.json({ id: 99 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + const basePayload = { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + }; + + await processJob(env, { + type: "github-webhook", + deliveryId: "bot-skip", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + pull_request: { number: 20, title: "Dependency update", state: "open", user: { login: "renovate[bot]", type: "Bot" }, labels: [], body: "" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "maintainer-skip", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + pull_request: { number: 21, title: "Maintainer work", state: "open", user: { login: "jsonbored" }, author_association: "OWNER", labels: [], body: "" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "check-enabled", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + pull_request: { number: 22, title: "Miner work", state: "open", user: { login: "oktofeesh1" }, head: { sha: "abc123" }, labels: [], body: "No issue needed." }, + }, + }); + + expect(calls).toEqual({ minerList: 1, checks: 1 }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ? order by created_at").bind("github_app.pr_visibility_skipped").all<{ + detail: string; + }>(); + expect(skipped.results.map((event) => event.detail)).toEqual(expect.arrayContaining(["bot_author", "maintainer_author"])); + }); + + it("audits advisory context check permission failures without blocking webhook processing", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "off", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor" }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/context403/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs")) return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "context-permission-missing", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 24, title: "Context check", state: "open", user: { login: "contributor" }, head: { sha: "context403" }, labels: [], body: "No issue needed." }, + }, + }); + + const audit = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") + .bind("github_app.check_run_permission_missing") + .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); + + expect(audit).toMatchObject({ + event_type: "github_app.check_run_permission_missing", + actor: "contributor", + target_key: "JSONbored/gittensory#24", + outcome: "error", + }); + expect(audit?.detail).toMatch(/Checks: write permission is missing/i); + }); + + it("audits advisory context check publish failures AND retries the job (GitHub 5xx is transient, GITTENSORY-5)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "off", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor" }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/context500/check-runs")) return new Response("GitHub check API failed", { status: 500 }); + return new Response("not found", { status: 404 }); + }); + const captureSpy = vi.spyOn(sentryModule, "captureReviewFailure"); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "context-check-failure", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 25, title: "Context check", state: "open", user: { login: "contributor" }, head: { sha: "context500" }, labels: [], body: "No issue needed." }, + }, + }), + ).rejects.toMatchObject({ retryKind: "public_surface_publish_transient" }); + + const outputFailure = await env.DB.prepare("select event_type, detail from audit_events where event_type = ?") + .bind("github_app.pr_check_run_publish_failed") + .first<{ event_type: string; detail: string }>(); + expect(outputFailure).toMatchObject({ event_type: "github_app.pr_check_run_publish_failed" }); + expect(outputFailure?.detail).toMatch(/GitHub check API failed|failed/i); + const aggregate = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_failed") + .first<{ detail: string; metadata_json: string }>(); + expect(aggregate).toMatchObject({ detail: "check_run" }); + expect(aggregate?.metadata_json).toContain('"output":"check_run"'); + expect(aggregate?.metadata_json).toContain('"transient":true'); + // The total publish failure (nothing reached the PR) escalates to Sentry at error level, not just the ledger — + // this still fires BEFORE the retryable throw, so the failure stays observable even though the job also retries. + expect(captureSpy).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({ kind: "publish", repo: "JSONbored/gittensory" })); + captureSpy.mockRestore(); + }); + + it("audits disabled public-surface skips without miner lookup", async () => { + const env = createTestEnv(); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + }); + const calls = { fetch: 0, repoWideReads: 0 }; + const originalDb = env.DB; + env.DB = new Proxy(originalDb, { + get(target, prop, receiver) { + if (prop !== "prepare") return Reflect.get(target, prop, receiver); + return (sql: string) => { + if (/from\s+["`]?issues["`]?/i.test(sql) || /from\s+["`]?bounties["`]?/i.test(sql)) calls.repoWideReads += 1; + return target.prepare(sql); + }; + }, + }) as D1Database; + vi.stubGlobal("fetch", async () => { + calls.fetch += 1; + return new Response("unexpected fetch", { status: 500 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", {}); + await processJob(env, { + type: "github-webhook", + deliveryId: "surface-off-skip", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 23, title: "Quiet repo work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "" }, + }, + }); + + expect(calls).toEqual({ fetch: 0, repoWideReads: 0 }); + const skipped = await env.DB.prepare("select actor, target_key, detail, metadata_json from audit_events where event_type = ?").bind("github_app.pr_visibility_skipped").all<{ + actor: string; + target_key: string; + detail: string; + metadata_json: string; + }>(); + expect(skipped.results).toEqual([ + expect.objectContaining({ + actor: "oktofeesh1", + target_key: "JSONbored/gittensory#23", + detail: "surface_off", + }), + ]); + expect(JSON.stringify(skipped.results)).not.toMatch(/wallet|hotkey|raw trust|installation-token/i); + }); + + it("records public comment failure without blocking the context check", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicSurface: "comment_only", + autoLabelEnabled: true, + createMissingLabel: true, + checkRunMode: "enabled", + checkRunDetailLevel: "standard", + }); + const calls = { checks: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/abc123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + calls.checks += 1; + return Response.json({ id: 42, html_url: "https://github.com/checks/42" }, { status: 201 }); + } + if (url.includes("/issues/30/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/30/comments") && method === "POST") return new Response("comment failed", { status: 503 }); + return new Response("not found", { status: 404 }); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "comment-failure", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 30, title: "Miner work", state: "open", head: { sha: "abc123", ref: "feature" }, user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }), + ).resolves.toBeUndefined(); + + expect(calls.checks).toBe(1); + const webhook = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("comment-failure").first<{ status: string }>(); + expect(webhook?.status).toBe("processed"); + const outputFailures = await env.DB.prepare("select event_type, detail from audit_events where target_key = ? and outcome = ? order by event_type") + .bind("JSONbored/gittensory#30", "error") + .all<{ event_type: string; detail: string }>(); + expect(outputFailures.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + event_type: "github_app.pr_comment_publish_failed", + detail: "comment failed", + }), + ]), + ); + const published = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").first<{ metadata_json: string }>(); + expect(published?.metadata_json).toContain('"publishedOutputs":["check_run"]'); + expect(published?.metadata_json).toContain('"output":"comment"'); + }); + + it("records an aggregate public-surface failure when no configured output publishes (permanent failure, no retry)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicSurface: "comment_only", + checkRunMode: "off", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/31/comments") && method === "GET") return Response.json([]); + // A 403 with no rate-limit signal (permissions revoked, not a burst limit) is PERMANENT: retrying forever + // would never converge, so this must keep today's swallow-and-audit behavior, not throw a retryable error. + if (url.includes("/issues/31/comments") && method === "POST") return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); + return new Response("not found", { status: 404 }); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "all-public-outputs-failed", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 31, title: "Miner work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }), + ).resolves.toBeUndefined(); + + const aggregate = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_failed") + .first<{ detail: string; metadata_json: string }>(); + expect(aggregate).toMatchObject({ detail: "comment" }); + expect(aggregate?.metadata_json).toContain('"output":"comment"'); + expect(aggregate?.metadata_json).toContain('"transient":false'); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").all(); + expect(published.results).toEqual([]); + const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("all-public-outputs-failed").first<{ status: string }>(); + expect(webhookRow?.status).toBe("processed"); + }); + + it("retries the whole job when a transient GitHub 5xx drops every public-surface output (GITTENSORY-5)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicSurface: "comment_only", + checkRunMode: "off", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/32/comments") && method === "GET") return Response.json([]); + // GitHub 5xx during publish: momentary, not the caller's fault — the job must retry, not silently drop the + // review the same way JSONbored/awesome-claude#4251 did (Sentry GITTENSORY-5). + if (url.includes("/issues/32/comments") && method === "POST") return new Response("upstream unavailable", { status: 502 }); + return new Response("not found", { status: 404 }); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "transient-publish-failure", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 32, title: "Miner work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }), + ).rejects.toMatchObject({ retryKind: "public_surface_publish_transient" }); + + // The failure IS still audited (observability doesn't regress) — it just also throws so the queue retries. + const aggregate = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_failed") + .first<{ detail: string; metadata_json: string }>(); + expect(aggregate).toMatchObject({ detail: "comment" }); + expect(aggregate?.metadata_json).toContain('"transient":true'); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").all(); + expect(published.results).toEqual([]); + // The webhook row is marked "error", not "processed" — a thrown job is exactly what lets the queue retry it. + const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("transient-publish-failure").first<{ status: string }>(); + expect(webhookRow?.status).toBe("error"); + }); + + it("leaves a fully successful public-surface publish unaffected by the transient-retry check", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicSurface: "comment_only", + checkRunMode: "off", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/33/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/33/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "public-surface-clean-publish", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 33, title: "Miner work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }), + ).resolves.toBeUndefined(); + + const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("public-surface-clean-publish").first<{ status: string }>(); + expect(webhookRow?.status).toBe("processed"); + const failed = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.pr_public_surface_failed").all(); + expect(failed.results).toEqual([]); + const published = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").first<{ metadata_json: string }>(); + expect(published?.metadata_json).toContain('"publishedOutputs":["comment"]'); + expect(published?.metadata_json).toContain('"failedOutputs":[]'); + }); + + it("keeps repository and PR webhook processing internal when installation context is absent", async () => { + const env = createTestEnv(); + await processJob(env, { + type: "github-webhook", + deliveryId: "repositories-without-installation", + eventName: "repository", + payload: { + action: "created", + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }], + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-without-installation", + eventName: "pull_request", + payload: { + action: "opened", + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 44, title: "Internal-only PR", state: "open", user: { login: "oktofeesh1" }, labels: [] }, + }, + }); + + expect(await listPullRequests(env, "JSONbored/gittensory")).toEqual(expect.arrayContaining([expect.objectContaining({ number: 44, body: null })])); + const events = await env.DB.prepare("select delivery_id, status from webhook_events where delivery_id in (?, ?) order by delivery_id") + .bind("pr-without-installation", "repositories-without-installation") + .all<{ delivery_id: string; status: string }>(); + expect(events.results).toEqual([ + { delivery_id: "pr-without-installation", status: "processed" }, + { delivery_id: "repositories-without-installation", status: "processed" }, + ]); + }); + + it("uses cached confirmed miner detection for label-only public surfaces", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + }); + const calls = { comments: 0, labels: 0, minerList: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/comments")) { + calls.comments += 1; + return Response.json([]); + } + if (url.includes("/labels") && method === "GET") return Response.json([]); + if (url.includes("/labels") && method === "POST") { + calls.labels += 1; + return Response.json([{ name: "gittensor" }]); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "label-only", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 45, title: "Miner label-only work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "label-only-cached", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 46, title: "Miner label-only follow-up", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }); + + // 2 PRs × 3 label POSTs each: the gittensor context label (apply) + the per-PR TYPE label (create + apply). + expect(calls).toEqual({ comments: 0, labels: 6, minerList: 1 }); + const cacheAudit = await env.DB.prepare("select event_type, detail from audit_events where actor = ? order by created_at") + .bind("oktofeesh1") + .all<{ event_type: string; detail: string | null }>(); + expect(cacheAudit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), + expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "confirmed" }), + ]), + ); + const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("oktofeesh1").first<{ status: string }>(); + expect(cached?.status).toBe("confirmed"); + const snapshot = await env.DB.prepare("select snapshot_json from official_miner_detections where login = ?").bind("oktofeesh1").first<{ snapshot_json: string }>(); + expect(snapshot?.snapshot_json).not.toContain("must-not-cache"); + }); + + it("records label-only public-surface failures without creating duplicate comments", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + }); + const calls = { comments: 0, labels: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/comments")) { + calls.comments += 1; + return Response.json([]); + } + if (url.includes("/labels") && method === "GET") return Response.json([]); + if (url.includes("/labels") && method === "POST") { + calls.labels += 1; + // A permanent failure (permissions gap, not a momentary blip) — this test is about duplicate-comment + // suppression on a label-only surface, not about retry classification, so it must stay non-transient. + return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); + } + return new Response("not found", { status: 404 }); + }); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "label-failure", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 50, title: "Miner label work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }), + ).resolves.toBeUndefined(); + + // gittensor context-label apply (fails 403, recorded) + the best-effort type-label create attempt (also 403, + // swallowed). The context-label failure is still recorded below; the type label never drops the recording. + expect(calls).toEqual({ comments: 0, labels: 2 }); + const outputFailure = await env.DB.prepare("select event_type, detail from audit_events where event_type = ?") + .bind("github_app.pr_label_publish_failed") + .first<{ event_type: string; detail: string }>(); + expect(outputFailure?.event_type).toBe("github_app.pr_label_publish_failed"); + expect(outputFailure?.detail).toMatch(/Resource not accessible by integration/); + const aggregate = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.pr_public_surface_failed") + .first<{ detail: string; metadata_json: string }>(); + expect(aggregate).toMatchObject({ detail: "label" }); + expect(aggregate?.metadata_json).toContain('"output":"label"'); + const published = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").all(); + expect(published.results).toEqual([]); + }); + + it("keeps GitHub-history-only contributors quiet through not_found cache hits and expiry", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 9, + title: "Historical merged work", + state: "closed", + merged_at: "2026-05-22T00:00:00.000Z", + user: { login: "newbie" }, + author_association: "NONE", + labels: [{ name: "feature" }], + body: "Previously merged.", + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicAudienceMode: "gittensor_only", + publicSurface: "comment_and_label", + autoLabelEnabled: true, + checkRunMode: "off", + }); + const calls = { minerList: 0, publicOutput: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens") || url.includes("/comments") || url.includes("/labels")) { + calls.publicOutput += 1; + return Response.json({}); + } + return new Response("not found", { status: 404 }); + }); + const basePayload = { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + }; + + for (const number of [47, 48]) { + await processJob(env, { + type: "github-webhook", + deliveryId: `not-found-cache-${number}`, + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number, title: "Contributor work", state: "open", user: { login: "newbie" }, labels: [], body: "Fixes #1" }, + }, + }); + } + await env.DB.prepare("update official_miner_detections set expires_at = ? where login = ?").bind("2000-01-01T00:00:00.000Z", "newbie").run(); + await processJob(env, { + type: "github-webhook", + deliveryId: "not-found-cache-expired", + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number: 49, title: "Contributor follow-up", state: "open", user: { login: "newbie" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(calls).toEqual({ minerList: 2, publicOutput: 0 }); + const audit = await env.DB.prepare("select event_type, detail from audit_events where actor = ? order by created_at") + .bind("newbie") + .all<{ event_type: string; detail: string | null }>(); + expect(audit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), + expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "not_found" }), + expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", detail: "not_official_gittensor_miner" }), + ]), + ); + const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("newbie").first<{ status: string }>(); + expect(cached?.status).toBe("not_found"); + }); + + it("checks official miner status for detected-only comments before publishing public output", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "oss_maintainer", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 3, + title: "Cached historical work", + state: "closed", + merged_at: "2026-05-20T00:00:00.000Z", + user: { login: "confirmed-dev" }, + labels: [], + body: "Historical cached PR.", + }); + + const calls = { minerList: 0, comments: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([ + { githubUsername: "confirmed-dev", githubId: "123", totalPrs: 2, totalMergedPrs: 1, isEligible: true, credibility: 1 }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/confirmed-dev")) return Response.json({ login: "confirmed-dev", public_repos: 1, followers: 0 }); + if (url.includes("/users/confirmed-dev/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/51/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/51/comments") && method === "POST") { + calls.comments += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; + expect(body.body).toContain("[Gittensor profile](https://gittensor.io/miners/details?githubId=123)"); + expect(body.body).toContain("2 PR(s)"); + expect(body.body).not.toContain("Cached prior PRs/issues"); + expect(body.body).not.toContain("api.gittensor.io/miners/123"); + return Response.json({ id: 51 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + const basePayload = { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + }; + + await processJob(env, { + type: "github-webhook", + deliveryId: "detected-comment-confirmed", + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number: 51, title: "Confirmed contributor work", state: "open", user: { login: "confirmed-dev" }, labels: [], body: "Fixes #1" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "detected-comment-not-found", + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number: 52, title: "Unconfirmed contributor work", state: "open", user: { login: "newbie" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(calls).toEqual({ minerList: 2, comments: 2 }); + }); + + it("fails closed when official miner detection is unavailable", async () => { + const env = createTestEnv(); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + publicAudienceMode: "gittensor_only", + }); + const payload = { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { + number: 10, + title: "Check run failure path", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "abc123" }, + labels: [], + body: "Fixes #1", + }, + }; + + const calls = { minerList: 0 }; + vi.stubGlobal("fetch", async () => { + calls.minerList += 1; + return new Response("gittensor unavailable", { status: 503 }); + }); + + await upsertRepoFocusManifest(env, "JSONbored/gittensory", {}); + await expect(processJob(env, { type: "github-webhook", deliveryId: "miner-unavailable", eventName: "pull_request", payload })).resolves.toBeUndefined(); + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "miner-unavailable-cached", + eventName: "pull_request", + payload: { ...payload, pull_request: { ...payload.pull_request, number: 11 } }, + }), + ).resolves.toBeUndefined(); + expect(calls.minerList).toBe(1); + const audit = await env.DB.prepare("select event_type, outcome, detail from audit_events where target_key = ?") + .bind("JSONbored/gittensory#10") + .all<{ event_type: string; outcome: string; detail: string }>(); + expect(audit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", outcome: "completed", detail: "miss" }), + expect.objectContaining({ event_type: "github_app.miner_detection_unavailable", outcome: "error", detail: expect.stringContaining("Gittensor API failed") }), + expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", outcome: "completed", detail: "miner_detection_unavailable" }), + ]), + ); + const cachedAudit = await env.DB.prepare("select event_type, outcome, detail from audit_events where target_key = ?") + .bind("JSONbored/gittensory#11") + .all<{ event_type: string; outcome: string; detail: string }>(); + expect(cachedAudit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", outcome: "completed", detail: "unavailable" }), + expect.objectContaining({ event_type: "github_app.miner_detection_unavailable", outcome: "error", detail: expect.stringContaining("Gittensor API failed") }), + expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", outcome: "completed", detail: "miner_detection_unavailable" }), + ]), + ); + const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("oktofeesh1").first<{ status: string }>(); + expect(cached?.status).toBe("unavailable"); + }); + + it("recovers confirmed miners after the unavailable cache window expires", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + }); + let officialSource: "down" | "confirmed" = "down"; + const calls = { minerList: 0, labels: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + if (officialSource === "down") return new Response("gittensor unavailable", { status: 503 }); + return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", hotkey: "must-not-cache", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/labels") && method === "GET") return Response.json([]); + if (url.includes("/labels") && method === "POST") { + calls.labels += 1; + return Response.json([{ name: "gittensor" }]); + } + return new Response("not found", { status: 404 }); + }); + const basePayload = { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + }; + + for (const number of [12, 13]) { + await processJob(env, { + type: "github-webhook", + deliveryId: `miner-unavailable-recovery-${number}`, + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number, title: "Miner recovery", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }); + } + expect(calls).toEqual({ minerList: 1, labels: 0 }); + await env.DB.prepare("update official_miner_detections set expires_at = ? where login = ?").bind("2000-01-01T00:00:00.000Z", "oktofeesh1").run(); + officialSource = "confirmed"; + + await processJob(env, { + type: "github-webhook", + deliveryId: "miner-unavailable-recovered", + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number: 14, title: "Miner recovery confirmed", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }); + + // 1 labeled PR × 3 label POSTs: the gittensor context label (apply) + the per-PR TYPE label (create + apply). + expect(calls).toEqual({ minerList: 2, labels: 3 }); + const cached = await env.DB.prepare("select status, snapshot_json from official_miner_detections where login = ?") + .bind("oktofeesh1") + .first<{ status: string; snapshot_json: string }>(); + expect(cached?.status).toBe("confirmed"); + expect(cached?.snapshot_json).not.toMatch(/hotkey|wallet|coldkey|must-not-cache/i); + }); + + it("suppresses labels and comments when agentPaused is true", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + publicSurface: "comment_and_label", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + agentPaused: true, + }); + const calls = { labels: 0, comments: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "paused-miner", githubId: "999", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners/999") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/999/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/999/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/paused-miner")) return Response.json({ login: "paused-miner" }); + if (url.includes("/users/paused-miner/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/labels") && method === "POST") { + calls.labels += 1; + return Response.json([{ name: "gittensor" }]); + } + if (url.includes("/comments") && method === "POST") { + calls.comments += 1; + return Response.json({ id: 1 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "paused-surface", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 88, title: "Paused repo PR", state: "open", user: { login: "paused-miner" }, labels: [], body: "Fixes #1" }, + }, + }); + + // agentPaused suppresses ALL public surface mutations — no label, no comment. + expect(calls).toEqual({ labels: 0, comments: 0 }); + }); + + it("responds to authorized @gittensory mention commands with one public-safe comment", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 77, + title: "Miner command context", + state: "open", + user: { login: "oktofeesh1" }, + author_association: "NONE", + labels: [], + body: "Fixes #1", + }); + const calls = { commentsCreated: 0, token: 0, minerList: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 3, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 3, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + // #788: Q&A commands now authorize by REAL repo permission. The "maintainer" commenter has maintain + // access; everyone else has none and is authorized only as pr_author/confirmed_miner where applicable. + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); + if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { + calls.commentsCreated += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; + expect(body.body).toContain(""); + expect(body.body).toContain("@gittensory"); + expect(body.body).not.toMatch(/wallet|hotkey|estimated score|reward estimate|payout|farming|raw trust score|private reviewability|reviewability internals|scoreability|public score estimate/i); + return Response.json({ id: 1001 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-miner-context", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 1, + body: "@gittensory miner-context", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-blockers", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 2, + body: "@gittensory blockers", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-help", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 3, + body: "@gittensory help", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-author-next-action", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 4, + body: "@gittensory next-action", + user: { login: "oktofeesh1", type: "User" }, + author_association: "NONE", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-reviewability", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 5, + body: "@gittensory reviewability", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-repo-fit", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 6, + body: "@gittensory repo-fit", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-packet", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 7, + body: "@gittensory packet", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-packet-no-cache", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 78, title: "Uncached PR command", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 8, + body: "@gittensory packet", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + + expect(calls.commentsCreated).toBe(8); + // The installation token is cached + reused across all 8 commands (each previously minted 2 — permission + // check + comment — for 16 total). Caching collapses them to a single mint, which is the rate-limit fix. + expect(calls.token).toBe(1); + expect(calls.minerList).toBeGreaterThanOrEqual(1); + const audit = await env.DB.prepare("select event_type, detail from audit_events where target_key = ? order by created_at") + .bind("JSONbored/gittensory#77") + .all<{ event_type: string; detail: string | null }>(); + expect(audit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.agent_command_replied" }), + expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), + expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "confirmed" }), + ]), + ); + const usage = await env.DB.prepare("select payload_json from signal_snapshots where signal_type = ? and target_key = ? order by generated_at") + .bind("github-agent-command-usage", "JSONbored/gittensory#77") + .all<{ payload_json: string }>(); + const usagePayloads = usage.results.map((entry) => JSON.parse(entry.payload_json) as { command: string; outcome: string; actorKind: string; actorHash?: string }); + expect(usagePayloads).toEqual( + expect.arrayContaining([ + expect.objectContaining({ command: "reviewability", outcome: "replied", actorKind: "maintainer" }), + expect.objectContaining({ command: "repo-fit", outcome: "replied", actorKind: "maintainer" }), + expect.objectContaining({ command: "packet", outcome: "replied", actorKind: "maintainer" }), + ]), + ); + expect(usagePayloads.every((payload) => typeof payload.actorHash === "string" && /^[a-f0-9]{64}$/.test(payload.actorHash))).toBe(true); + expect(JSON.stringify(usagePayloads)).not.toContain('"actor":'); + expect(JSON.stringify(usagePayloads)).not.toMatch(/wallet|hotkey|raw trust score|payout|reward estimate|farming|private reviewability|public score estimate|@gittensory|oktofeesh1/i); + const usageEvents = await listProductUsageEvents(env, { limit: 10 }); + expect(usageEvents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ surface: "github_app", eventName: "agent_command_replied", outcome: "completed", repoFullName: "JSONbored/gittensory" }), + ]), + ); + expect(JSON.stringify(usageEvents)).not.toMatch(/wallet|hotkey|raw trust|deliveryId|installation-token/i); + }); + + it("a @gittensory Q&A mention command respects agentPaused — never posts the answer card live (#2258)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 77, + title: "Paused Q&A context", + state: "open", + user: { login: "oktofeesh1" }, + author_association: "NONE", + labels: [], + body: "Fixes #1", + }); + const calls = { commentPosts: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); + if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { + calls.commentPosts += 1; + return Response.json({ id: 1001 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-help-paused", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Paused Q&A context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { id: 1, body: "@gittensory help", user: { login: "maintainer", type: "User" }, author_association: "OWNER" }, + }, + }); + + expect(calls.commentPosts).toBe(0); // the answer card must never post live on a paused repo + // REGRESSION: a paused command must not be audited/usage-tracked as a real, completed reply. + const replied = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.agent_command_replied").first<{ id: string }>(); + expect(replied).toBeUndefined(); + const skipped = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.agent_command_reply_skipped").first<{ outcome: string; detail: string }>(); + expect(skipped).toMatchObject({ outcome: "completed", detail: "agent_paused" }); + const usageEvents = await listProductUsageEvents(env, { limit: 10 }); + expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ eventName: "agent_command_reply_skipped", outcome: "skipped" })])); + expect(usageEvents.some((event) => event.eventName === "agent_command_replied")).toBe(false); + }); + + it("a @gittensory maintainer-digest command respects agentDryRun — records dry_run, not agent_paused (#2258)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 78, + title: "Dry-run digest context", + state: "open", + user: { login: "oktofeesh1" }, + author_association: "NONE", + labels: [], + body: "Fixes #1", + }); + const calls = { commentPosts: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); + if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { + calls.commentPosts += 1; + return Response.json({ id: 1002 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-queue-summary-dry-run", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 78, title: "Dry-run digest context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { id: 2, body: "@gittensory queue-summary", user: { login: "maintainer", type: "User" }, author_association: "OWNER" }, + }, + }); + + expect(calls.commentPosts).toBe(0); // the digest must never post live on a dry-run repo + const skipped = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.agent_command_reply_skipped") + .first<{ outcome: string; detail: string; metadata_json: string }>(); + expect(skipped).toMatchObject({ outcome: "completed", detail: "dry_run" }); + const usageEvents = await listProductUsageEvents(env, { limit: 10 }); + expect(usageEvents).toEqual( + expect.arrayContaining([expect.objectContaining({ eventName: "agent_command_reply_skipped", outcome: "skipped", metadata: expect.objectContaining({ family: "queue_digest" }) })]), + ); + }); + + it("posts maintainer-only queue digest commands from cached public-safe metadata", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + delete (env as Partial).PUBLIC_SITE_ORIGIN; + for (const issue of [ + { number: 1, title: "Ready linked fix" }, + { number: 2, title: "Overlap issue" }, + ]) { + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { + number: issue.number, + title: issue.title, + state: "open", + user: { login: "reporter" }, + labels: [], + body: "", + }); + } + for (const pull of [ + { number: 90, title: "Ready linked fix", user: { login: "alice" }, body: "Fixes #1" }, + { number: 91, title: "Needs author context", user: { login: "bob" }, body: "" }, + { number: 92, title: "Overlap route first", user: { login: "carol" }, body: "Fixes #2" }, + { number: 93, title: "Overlap route second", user: { login: "dana" }, body: "Fixes #2" }, + ]) { + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + ...pull, + state: "open", + author_association: "NONE", + labels: [], + }); + } + await upsertOfficialMinerDetection(env, "alice", { status: "confirmed", snapshot: queueMinerSnapshot("alice") }, 60_000); + + const calls = { commentsCreated: 0, token: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); // #788 real-permission auth + if (url.includes("/issues/90/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/90/comments") && method === "POST") { + calls.commentsCreated += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; + expect(body.body).toContain("**Gittensory maintainer queue summary**"); + expect(body.body).toContain("Open PRs: 4"); + expect(body.body).toContain("confirmed-miner PRs: 1"); + expect(body.body).toContain("Authenticated control panel: https://gittensory.aethereal.dev/app?view=maintainer&repo=JSONbored%2Fgittensory"); + expect(body.body).not.toMatch(/wallet|hotkey|raw trust score|payout|reward estimate|farming|private reviewability|public score estimate/i); + return Response.json({ id: 1001 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "maintainer-queue-summary", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 90, title: "Ready linked fix", state: "open", pull_request: {}, user: { login: "alice" }, author_association: "NONE" }, + comment: { + id: 9001, + body: "@gittensory queue-summary", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + + expect(calls).toEqual({ commentsCreated: 1, token: 1 }); // token cached + reused across the #788 permission check + const audit = await env.DB.prepare("select event_type, detail, metadata_json from audit_events where target_key = ? order by created_at") + .bind("JSONbored/gittensory#90") + .all<{ event_type: string; detail: string | null; metadata_json: string }>(); + expect(audit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.agent_command_replied" }), + expect.objectContaining({ event_type: "github_app.agent_command_feedback_prompted", detail: "queue-summary" }), + ]), + ); + expect(audit.results.find((entry) => entry.event_type === "github_app.agent_command_feedback_prompted")?.metadata_json).toContain("maintainer_digest"); + const usage = await env.DB.prepare("select payload_json from signal_snapshots where signal_type = ? and target_key = ?") + .bind("github-agent-command-usage", "JSONbored/gittensory#90") + .all<{ payload_json: string }>(); + const usagePayload = JSON.parse(usage.results[0]?.payload_json ?? "{}") as { command?: string; outcome?: string; family?: string; actorHash?: string }; + expect(usagePayload).toEqual(expect.objectContaining({ command: "queue-summary", outcome: "replied", family: "maintainer_digest" })); + expect(usagePayload.actorHash).toMatch(/^[a-f0-9]{64}$/); + expect(JSON.stringify(usagePayload)).not.toContain('"actor":'); + const usageEvents = await listProductUsageEvents(env, { limit: 5 }); + expect(usageEvents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ surface: "github_app", eventName: "agent_command_replied", outcome: "completed", metadata: expect.objectContaining({ family: "queue_digest" }) }), + ]), + ); + }); + + it("omits the maintainer queue digest control-panel link when the public site origin is invalid", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), PUBLIC_SITE_ORIGIN: "not a url" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 94, + title: "Ready linked fix", + state: "open", + author_association: "NONE", + user: { login: "alice" }, + labels: [], + body: "Fixes #1", + }); + let commentBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); // #788 real-permission auth + if (url.includes("/issues/94/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/94/comments") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; + commentBody = body.body ?? ""; + return Response.json({ id: 1002 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "maintainer-queue-summary-invalid-origin", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 94, title: "Ready linked fix", state: "open", pull_request: {}, user: { login: "alice" }, author_association: "NONE" }, + comment: { + id: 9002, + body: "@gittensory queue-summary", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + + expect(commentBody).toContain("**Gittensory maintainer queue summary**"); + expect(commentBody).not.toContain("Authenticated control panel:"); + }); + + it("applies repo command authorization policy overrides during issue_comment handling", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commandAuthorization: { default: ["maintainer"], commands: { help: ["pr_author"] } }, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 91, + title: "Author policy command", + state: "open", + user: { login: "driveby" }, + author_association: "NONE", + labels: [], + body: "Fixes #90", + }); + + const calls = { commentsCreated: 0, token: 0, minerList: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + // #788: "driveby" has no repo permission — authorized only as pr_author via the help-command override. + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); + if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { + calls.commentsCreated += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; + expect(body.body).toContain(""); + expect(body.body).not.toMatch(/wallet|hotkey|estimated score|reward estimate|payout|farming|raw trust score|private reviewability|public score estimate/i); + return Response.json({ id: 9191 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-policy-author", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 91, title: "Author policy command", state: "open", pull_request: {}, user: { login: "driveby" }, author_association: "NONE" }, + comment: { + id: 191, + body: "@gittensory help", + user: { login: "driveby", type: "User" }, + author_association: "NONE", + }, + }, + }); + + expect(calls).toEqual({ commentsCreated: 1, token: 1, minerList: 0 }); // token cached + reused across the #788 permission check + const audit = await env.DB.prepare("select event_type, detail from audit_events where target_key = ? order by created_at") + .bind("JSONbored/gittensory#91") + .all<{ event_type: string; detail: string | null }>(); + expect(audit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.agent_command_replied", detail: null }), + expect.objectContaining({ event_type: "github_app.agent_command_feedback_prompted", detail: "help" }), + ]), + ); + const usage = await env.DB.prepare("select payload_json from signal_snapshots where signal_type = ? and target_key = ?") + .bind("github-agent-command-usage", "JSONbored/gittensory#91") + .all<{ payload_json: string }>(); + expect(JSON.parse(usage.results[0]?.payload_json ?? "{}")).toMatchObject({ command: "help", outcome: "replied", actorKind: "author" }); + }); + + it("records deduped @gittensory answer usefulness from authorized reactions only", async () => { + const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "maintainer" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 77, + title: "Miner command context", + state: "open", + user: { login: "oktofeesh1" }, + author_association: "NONE", + }); + await upsertOfficialMinerDetection(env, "oktofeesh1", { status: "confirmed", snapshot: queueMinerSnapshot("oktofeesh1") }, 60 * 60 * 1000); + await upsertAgentCommandAnswer(env, commandAnswer("answer-maintainer", "preflight", { responseCommentId: 9001 })); + await upsertAgentCommandAnswer(env, commandAnswer("answer-author", "next-action", { responseCommentId: 9002 })); + await upsertAgentCommandAnswer(env, { ...commandAnswer("answer-no-author", "preflight", { responseCommentId: 9003 }), issueNumber: 78 }); + const basePayload = { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + }; + + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-1", + eventName: "reaction", + payload: { + ...basePayload, + comment: { id: 9001, body: commandAnswerBody("answer-maintainer", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 1, content: "+1", user: { login: "maintainer", type: "User" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-2", + eventName: "reaction", + payload: { + ...basePayload, + comment: { id: 9001, body: commandAnswerBody("answer-maintainer", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 2, content: "-1", user: { login: "maintainer", type: "User" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-3", + eventName: "reaction", + payload: { + ...basePayload, + comment: { id: 9002, body: commandAnswerBody("answer-author", "next-action"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 3, content: "+1", user: { login: "oktofeesh1", type: "User" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-4", + eventName: "reaction", + payload: { + ...basePayload, + comment: { id: 9002, body: commandAnswerBody("answer-author", "next-action"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 4, content: "+1", user: { login: "random", type: "User" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-5", + eventName: "reaction", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 78, title: "No author", state: "open", pull_request: {}, author_association: "NONE" }, + comment: { id: 9003, body: commandAnswerBody("answer-no-author", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 5, content: "+1", user: { login: "random", type: "User" } }, + }, + }); + + const summary = await getCommandUsefulnessSummary(env, { now: "2026-05-29T00:00:00.000Z", windowDays: 30 }); + expect(summary.totals).toMatchObject({ feedbackCount: 2, usefulCount: 1, notUsefulCount: 1, answerCount: 2, usefulnessRate: 0.5 }); + expect(summary.commands).toEqual([ + expect.objectContaining({ command: "next-action", feedbackCount: 1, usefulCount: 1 }), + expect.objectContaining({ command: "preflight", feedbackCount: 1, notUsefulCount: 1 }), + ]); + const audit = await env.DB.prepare("select event_type, detail from audit_events where event_type like ? order by created_at") + .bind("github_app.agent_command_feedback_%") + .all<{ event_type: string; detail: string | null }>(); + expect(audit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.agent_command_feedback_recorded" }), + expect.objectContaining({ event_type: "github_app.agent_command_feedback_denied", detail: "not_maintainer_or_pr_author" }), + ]), + ); + const stored = await env.DB.prepare("select actor_hash, metadata_json from github_agent_command_feedback").all<{ actor_hash: string; metadata_json: string }>(); + expect(stored.results.map((row) => row.actor_hash).join("\n")).not.toMatch(/maintainer|oktofeesh1|random/); + expect(stored.results.every((row) => row.actor_hash.startsWith("sha256:"))).toBe(true); + }); + + it("skips unsupported @gittensory feedback reactions without storing votes", async () => { + const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "maintainer" }); + await upsertAgentCommandAnswer(env, commandAnswer("answer-skip", "preflight", { responseCommentId: 9001 })); + const basePayload = { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + }; + + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-skip-1", + eventName: "reaction", + payload: { + ...basePayload, + action: "deleted", + comment: { id: 9001, body: commandAnswerBody("answer-skip", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 1, content: "+1", user: { login: "maintainer", type: "User" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-skip-1b", + eventName: "reaction", + payload: { + ...basePayload, + comment: { id: 9001, body: commandAnswerBody("answer-skip", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 11, content: "+1", user: { login: "maintainer", type: "User" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-skip-2", + eventName: "reaction", + payload: { + ...basePayload, + action: "created", + comment: { id: 9001, body: commandAnswerBody("answer-skip", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 2, content: "+1", user: { login: "helper[bot]", type: "Bot" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-skip-3", + eventName: "reaction", + payload: { + ...basePayload, + action: "created", + comment: { id: 9001, body: commandAnswerBody("answer-missing", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + reaction: { id: 3, content: "-1", user: { login: "maintainer", type: "User" } }, + }, + }); + + await expect(getCommandUsefulnessSummary(env, { now: "2026-05-29T00:00:00.000Z", windowDays: 30 })).resolves.toMatchObject({ + totals: { feedbackCount: 0 }, + commands: [], + }); + const skips = await env.DB.prepare("select detail from audit_events where event_type = ? order by detail") + .bind("github_app.agent_command_feedback_skipped") + .all<{ detail: string }>(); + expect(skips.results.map((row) => row.detail)).toEqual(["bot_reaction", "unknown_answer", "unsupported_reaction_action", "unsupported_reaction_action"]); + }); + + it("accepts repo-owner feedback through sender fallback and ignores non-vote reactions", async () => { + const env = createTestEnv(); + await upsertAgentCommandAnswer(env, commandAnswer("answer-owner", "blockers")); + const basePayload = { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { id: 9001, body: commandAnswerBody("answer-owner", "blockers"), user: { login: "gittensory[bot]", type: "Bot" } }, + }; + + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-owner-1", + eventName: "reaction", + payload: { + ...basePayload, + reaction: { content: "+1" }, + sender: { login: "JSONbored", type: "User" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-owner-2", + eventName: "reaction", + payload: { + ...basePayload, + reaction: { id: 2, content: "heart" }, + sender: { login: "JSONbored", type: "User" }, + }, + }); + + const summary = await getCommandUsefulnessSummary(env, { now: "2026-05-29T00:00:00.000Z", windowDays: 30 }); + expect(summary.totals).toMatchObject({ feedbackCount: 1, usefulCount: 1, answerCount: 1 }); + expect(summary.commands).toEqual([expect.objectContaining({ command: "blockers", usefulnessRate: 1 })]); + }); + + it("rejects copied feedback markers that do not match the stored answer context", async () => { + const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "maintainer" }); + await upsertAgentCommandAnswer(env, commandAnswer("answer-bound", "preflight", { responseCommentId: 9001 })); + const basePayload = { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + reaction: { id: 1, content: "+1", user: { login: "maintainer", type: "User" } }, + }; + + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-bound-1", + eventName: "reaction", + payload: { + ...basePayload, + comment: { id: 9002, body: commandAnswerBody("answer-bound", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-bound-2", + eventName: "reaction", + payload: { + ...basePayload, + repository: { name: "other", full_name: "JSONbored/other", private: false, owner: { login: "JSONbored" } }, + comment: { id: 9001, body: commandAnswerBody("answer-bound", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "feedback-bound-3", + eventName: "reaction", + payload: { + ...basePayload, + issue: { ...basePayload.issue, number: 78 }, + comment: { id: 9001, body: commandAnswerBody("answer-bound", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, + }, + }); + + await expect(getCommandUsefulnessSummary(env, { now: "2026-05-29T00:00:00.000Z", windowDays: 30 })).resolves.toMatchObject({ + totals: { feedbackCount: 0 }, + commands: [], + }); + const skips = await env.DB.prepare("select detail from audit_events where event_type = ? order by detail") + .bind("github_app.agent_command_feedback_skipped") + .all<{ detail: string }>(); + expect(skips.results.map((row) => row.detail)).toEqual(["answer_comment_mismatch", "answer_context_mismatch", "answer_context_mismatch"]); + }); + + it("records webhook errors when command replies fail before mutation", async () => { + const env = createTestEnv(); + // Authorize via the confirmed-miner path (PR author + cached confirmed status), which does not depend on + // the #788 real-permission check — that check swallows the invalid-repo error and would deny otherwise. + await upsertOfficialMinerDetection(env, "oktofeesh1", { status: "confirmed", snapshot: queueMinerSnapshot("oktofeesh1") }, 60_000); + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-error", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "broken", full_name: "broken", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Broken command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 9, + body: "@gittensory help", + user: { login: "oktofeesh1", type: "User" }, + author_association: "NONE", + }, + }, + }), + ).rejects.toThrow("Invalid repository full name"); + + const event = await env.DB.prepare("select status, error_summary from webhook_events where delivery_id = ?") + .bind("agent-command-error") + .first<{ status: string; error_summary: string }>(); + expect(event).toMatchObject({ status: "error", error_summary: expect.stringContaining("Invalid repository full name") }); + }); + + it("skips unauthorized, bot, and non-PR @gittensory mention commands without public output", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + let commentCalls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/issues/")) { + commentCalls += 1; + return Response.json([]); + } + return new Response("not found", { status: 404 }); + }); + const basePayload = { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + }; + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-none", + eventName: "issue_comment", + payload: { + ...basePayload, + issue: { number: 79, title: "No command", state: "open", pull_request: {}, user: { login: "reporter" } }, + comment: { id: 0, body: "plain comment", user: { login: "reporter", type: "User" }, author_association: "NONE" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-missing-fields", + eventName: "issue_comment", + payload: { + action: "created", + comment: { id: 9, body: "@gittensory preflight", user: { login: "reporter", type: "User" }, author_association: "NONE" }, + }, + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-non-pr", + eventName: "issue_comment", + payload: { + ...basePayload, + issue: { number: 80, title: "Plain issue", state: "open", user: { login: "reporter" } }, + comment: { id: 1, body: "@gittensory preflight", user: { login: "reporter", type: "User" }, author_association: "NONE" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-bot", + eventName: "issue_comment", + payload: { + ...basePayload, + issue: { number: 81, title: "Bot PR", state: "open", pull_request: {}, user: { login: "renovate[bot]" } }, + comment: { id: 2, body: "@gittensory preflight", user: { login: "renovate[bot]", type: "Bot" }, author_association: "NONE" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-unauthorized", + eventName: "issue_comment", + payload: { + ...basePayload, + issue: { number: 82, title: "Unauthorized PR", state: "open", pull_request: {}, user: { login: "not-a-miner" }, author_association: "NONE" }, + comment: { id: 3, body: "@gittensory preflight", user: { login: "not-a-miner", type: "User" }, author_association: "NONE" }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-no-pr-author", + eventName: "issue_comment", + payload: { + ...basePayload, + issue: { number: 83, title: "Unknown author PR", state: "open", pull_request: {}, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory preflight", user: { login: "commenter", type: "User" } }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-maintainer-only-denied", + eventName: "issue_comment", + payload: { + ...basePayload, + issue: { number: 84, title: "Maintainer digest PR", state: "open", pull_request: {}, user: { login: "not-a-miner" }, author_association: "NONE" }, + comment: { id: 5, body: "@gittensory queue-summary", user: { login: "not-a-miner", type: "User" }, author_association: "NONE" }, + }, + }); + + expect(commentCalls).toBe(0); + const skips = await env.DB.prepare("select detail from audit_events where event_type = ? order by detail") + .bind("github_app.agent_command_skipped") + .all<{ detail: string }>(); + expect(skips.results.map((entry) => entry.detail)).toEqual(expect.arrayContaining(["bot_author", "maintainer_command_requires_maintainer", "not_a_pull_request_thread", "pr_author_not_confirmed_miner"])); + const usageEvents = await listProductUsageEvents(env, { limit: 10 }); + expect(usageEvents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ surface: "github_app", eventName: "agent_command_skipped", outcome: "skipped" }), + ]), + ); + expect(JSON.stringify(usageEvents)).not.toMatch(/deliveryId|wallet|hotkey|raw trust/i); + }); + +}); diff --git a/test/unit/queue-5.test.ts b/test/unit/queue-5.test.ts new file mode 100644 index 0000000000..6a54c22026 --- /dev/null +++ b/test/unit/queue-5.test.ts @@ -0,0 +1,5969 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { generateKeyPairSync } from "node:crypto"; +import { clearInstallationTokenCacheForTest } from "../../src/github/app"; +import { clearReviewSuppressionCacheForTest } from "../../src/review/review-memory-wire"; +import { PR_PANEL_COMMENT_MARKER } from "../../src/github/comments"; +import * as backfillModule from "../../src/github/backfill"; +import * as rateLimitModule from "../../src/github/rate-limit"; +import * as repositoriesModule from "../../src/db/repositories"; +import * as reviewEffortModule from "../../src/review/review-effort"; +import * as repositorySettingsModule from "../../src/settings/repository-settings"; +import * as sentryModule from "../../src/selfhost/sentry"; +import { renderMetrics, resetMetrics } from "../../src/selfhost/metrics"; +import { jobCoalesceKey } from "../../src/selfhost/queue-common"; +import { + listCollisionEdges, + createAgentRun, + getCommandUsefulnessSummary, + getBurdenForecast, + getContributorEvidence, + getAgentRun, + getContributorScoringProfile, + getWebhookEvent, + getInstallation, + getLatestUpstreamRulesetSnapshot, + getPullRequest, + getPullRequestDetailSyncState, + upsertPullRequestDetailSyncState, + getRepository, + listUpstreamDriftReports, + listInstallationHealth, + listProductUsageDailyRollups, + listProductUsageEvents, + listPullRequests, + listPullRequestFiles, + listRepoSyncStates, + listSignalSnapshots, + persistSignalSnapshot, + recordGateBlockOutcome, + markGateOutcomeOverridden, + recordProductUsageEvent, + upsertAgentCommandAnswer, + upsertCheckSummary, + upsertIssueFromGitHub, + upsertRepoSyncSegment, + upsertInstallation, + updatePullRequestSlopAssessment, + upsertOfficialMinerDetection, + upsertPullRequestFile, + upsertPullRequestFromGitHub, + upsertIssueWatchSubscription, + upsertRepositoryAiKey, + upsertRepositorySettings, + upsertRepositoryFromGitHub, + putCachedAiReview, + markAiReviewPublished, + putCachedAiSlopAdvisory, + putCachedLinkedIssueSatisfaction, + recordReviewSuppression, + listReviewSuppressions, + setGlobalAgentFrozen, +} from "../../src/db/repositories"; +import { agentMaintenanceHeadMatchesGate, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors"; +import type { PullRequestRecord } from "../../src/types"; +import { aiReviewCacheInputFingerprint } from "../../src/review/ai-review-cache-input"; +import { fingerprint as reviewMemoryFingerprint } from "../../src/review/review-memory-match"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; +import * as focusManifestLoaderModule from "../../src/signals/focus-manifest-loader"; +import { normalizeRegistryPayload } from "../../src/registry/normalize"; +import { persistRegistrySnapshot } from "../../src/registry/sync"; +import { + classifyPullRequestFreshness, + fetchPullRequestFreshness, +} from "../../src/github/pr-freshness"; +import { createTestEnv } from "../helpers/d1"; +import { ISSUE_WAKE_MAX_PRS, MERGE_WAKE_MAX_PRS, SWEEP_MAX_PRS } from "../../src/settings/agent-sweep"; +import { AGENT_LABEL_PENDING_CLOSURE, DEFAULT_LINKED_ISSUE_HARD_RULES } from "../../src/review/linked-issue-hard-rules"; + +vi.mock("../../src/github/pr-freshness", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchPullRequestFreshness: vi.fn(async (_env: Env, args: { expectedHeadSha?: string | null }) => ({ + status: "current" as const, + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [] as string[], + })), + }; +}); + +// The re-gate sweep now FANS OUT the heavy re-review + marker stamp into per-PR `agent-regate-pr` jobs +// (#audit-sweep-fanout). A test asserting the re-review/stamp side effects must run the sweep AND drain the +// per-PR jobs it enqueues. Returns the captured agent-regate-pr jobs for assertions. +async function sweepAndDrainPerPr(env: Env, repoFullName: string): Promise { + const fanned: import("../../src/types").JobMessage[] = []; + const send = env.JOBS.send.bind(env.JOBS); + env.JOBS.send = (async (message: import("../../src/types").JobMessage, options?: QueueSendOptions) => { + if (message.type === "agent-regate-pr") fanned.push(message); + return send(message, options); + }) as typeof env.JOBS.send; + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName }); + env.JOBS.send = send; + for (const job of fanned) await processJob(env, job); + return fanned; +} + + +function completeSegment(repoFullName: string, segment: "labels" | "open_issues" | "open_pull_requests") { + return { + repoFullName, + segment, + status: "complete" as const, + sourceKind: "test" as const, + mode: "resume" as const, + fetchedCount: 1, + expectedCount: 1, + pageCount: 1, + completedAt: "2026-05-25T00:00:00.000Z", + warnings: [], + }; +} + +type CommandAnswerFixture = Parameters[1]; + +function commandAnswer(id: string, command: string, overrides: Partial = {}): CommandAnswerFixture { + return { + id, + repoFullName: "JSONbored/gittensory", + issueNumber: 77, + command, + requestCommentId: 7, + responseCommentId: 9001, + responseUrl: "https://github.com/JSONbored/gittensory/pull/77#issuecomment-9001", + actorKind: "maintainer" as const, + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + metadata: {}, + ...overrides, + }; +} + +function commandAnswerBody(answerId: string, command: string): string { + return [ + "", + ``, + `Command: \`@gittensory ${command}\``, + "Feedback is aggregate-only.", + ].join("\n"); +} + +function queueMinerSnapshot(login: string) { + return { + source: "gittensor_api" as const, + githubId: "123", + githubUsername: login, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + issueDiscoveryScore: 0, + issueTokenScore: 0, + issueCredibility: 1, + isIssueEligible: false, + issueEligibleRepoCount: 0, + alphaPerDay: 0, + taoPerDay: 0, + usdPerDay: 0, + totals: { + pullRequests: 3, + mergedPullRequests: 2, + openPullRequests: 1, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + }, + repositories: [], + pullRequests: [], + issueLabels: [], + }; +} + +function b64(value: string): string { + return Buffer.from(value, "utf8").toString("base64"); +} + +function withProductUsageInsertFailure(env: Env): Env { + const db = env.DB as unknown as { prepare(sql: string): unknown; batch(statements: unknown[]): Promise }; + return { + ...env, + DB: { + prepare(sql: string) { + if (sql.includes("product_usage_events")) throw new Error("product usage insert failed"); + return db.prepare.call(db, sql); + }, + batch(statements: unknown[]) { + return db.batch.call(db, statements); + }, + } as unknown as D1Database, + }; +} + +async function generatePrivateKeyPem(): Promise { + const key = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); + const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; +} + +describe("queue processors", () => { + // Freshness-SLO fixtures are dated relative to late May 2026; pin the clock so staleness windows + // stay deterministic regardless of when CI runs. + beforeEach(() => { + clearInstallationTokenCacheForTest(); + clearReviewSuppressionCacheForTest(); + vi.mocked(fetchPullRequestFreshness).mockReset(); + vi.mocked(fetchPullRequestFreshness).mockImplementation(async (_env, args) => ({ + status: "current", + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [], + })); + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-05-28T00:00:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + describe("review-nag cooldown (#2463)", () => { + // Reusable stub covering everything the normal @gittensory Q&A dispatch needs (token, collaborator + // permission, comment GET/search + POST) PLUS the maintenance close path (label GET/POST, PR PATCH) — + // a superset so every scenario below (fall-through OR short-circuit) can share one fetch handler. + function stubReviewNagFetch(prNumber: number, seen: { comments: string[]; labels: string[]; closed: boolean }) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); + if (url.endsWith(`/pulls/${prNumber}`) && method === "PATCH") { + seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; + return Response.json({ number: prNumber, state: "closed" }); + } + if (url.endsWith(`/pulls/${prNumber}`)) return Response.json({ number: prNumber, state: "open", head: { sha: `sha${prNumber}` }, mergeable_state: "clean" }); + if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + // Repo-level label definition (createMissingLabel: true probes/creates the label before applying it). + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); + if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { + seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); + return Response.json({ id: seen.comments.length }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + } + + it("is off by default — no ping is tracked and no cooldown action fires", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 200, title: "Off by default", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(200, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-off-default", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 200, title: "Off by default", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(0); + expect(seen.closed).toBe(false); + }); + + it("REGRESSION (gate-flagged): caps an oversized review-nag cooldown at MAX_REVIEW_NAG_COOLDOWN_DAYS before Date arithmetic, even when the resolved settings object itself carries an oversized value", async () => { + // upsertRepositorySettings/getRepositorySettings both clamp reviewNagCooldownDays on write AND read, so + // seeding an oversized value through the normal repository layer (even via a raw DB update bypassing the + // write-time clamp) can never actually reach maybeThrottleReviewNagPing uncapped -- the read-time clamp in + // 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 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 + // count; if the guard were removed, the uncapped window would count all three, crossing maxPings=3. + vi.setSystemTime(new Date("2025-04-24T00:00:00.000Z")); + for (let i = 0; i < 3; i += 1) { + await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#206", outcome: "completed" }); + } + vi.setSystemTime(new Date("2026-05-29T00:00:00.000Z")); // ~400 days later + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + const resolveSettingsSpy = vi + .spyOn(repositorySettingsModule, "resolveRepositorySettings") + .mockResolvedValueOnce({ ...baseSettings, reviewNagCooldownDays: 1_000_000_000 }); + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(206, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-huge-cooldown", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 206, title: "Huge cooldown", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + + // The 400-day-old pings fell outside the CAPPED 365-day window, so this is only the 1st ping this + // window — under maxPings=3, never throttled. An uncapped window would have counted all 3 prior pings + // (pingCount=4 > maxPings=3) and applied the cooldown instead. + const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); + expect(applied?.n).toBe(0); + expect(seen.closed).toBe(false); + expect(seen.comments.some((c) => c.includes("cooldown limit"))).toBe(false); + expect(resolveSettingsSpy).toHaveBeenCalled(); + resolveSettingsSpy.mockRestore(); + }); + + it("records pings under the configured threshold without acting; the normal @gittensory reply still proceeds", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", 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); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-under-threshold", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 201, title: "Under threshold", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(1); // the ping is recorded (1st of 3 allowed) + const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); + expect(applied?.n).toBe(0); // but no cooldown action — under threshold + expect(seen.closed).toBe(false); + // The review-nag hook returned false (fell through) — proven by the NORMAL mention-command dispatch + // making its own (here: unauthorized-skip) decision, rather than review-nag's short-circuit ever firing. + const skipped = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.agent_command_skipped'").first<{ n: number }>(); + expect(skipped?.n).toBeGreaterThanOrEqual(1); + }); + + 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(202, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-hold", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 202, title: "Hold cooldown", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); + expect(seen.comments.some((c) => c.includes("cooldown limit"))).toBe(true); + // Only ONE comment posted — the short-circuit skipped the normal answer-card dispatch. + expect(seen.comments).toHaveLength(1); + const applied = await env.DB.prepare("select outcome, detail from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ outcome: string; detail: string }>(); + expect(applied?.outcome).toBe("completed"); + expect(applied?.detail).toContain("hold applied"); + }); + + it("close policy on a PR thread: labels + closes once the threshold is crossed, with no merit review", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(203, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-close", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 203, title: "Close cooldown", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(true); + expect(seen.labels).toContain("too-chatty"); // configurable label, not hardcoded + expect(seen.comments.some((c) => c.includes("chatty") && c.includes("4 times"))).toBe(true); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + }); + + it("REGRESSION (#review-nag-cross-pr-carryover): a contributor who exhausted their pings on PR A carries the count over to a BRAND-NEW PR B instead of resetting to a clean 0/maxPings slate", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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" } }); + // 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: "" }); + for (let i = 0; i < 3; i += 1) { + await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#220", outcome: "completed" }); + } + // PR B: a BRAND-NEW PR from the SAME contributor -- a new issue.number means a new targetKey the old + // per-target count would treat as a clean slate. Only ONE ping is sent here. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 221, title: "PR B (brand new)", state: "open", user: { login: "chatty" }, head: { sha: "sha221" }, author_association: "NONE", labels: [], body: "" }); + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(221, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-carryover-pr-b", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 221, title: "PR B (brand new)", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + // Under the OLD per-targetKey count, this is ping 1/3 on PR B alone -- under threshold, no action. The + // FIX counts every prior ping across the whole repo, so this single PR-B ping is already #4 overall + // (3 carried over from PR A + this one), crossing maxPings=3 on the very first PR-B ping. + expect(seen.closed).toBe(true); + expect(seen.comments.some((c) => c.includes("chatty") && c.includes("4 times"))).toBe(true); + const prA = await env.DB.prepare("select state from pull_requests where number = 220").first<{ state: string }>(); + expect(prA?.state).toBe("closed"); // PR A is untouched by this second evaluation + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + }); + + 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 }); + 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(204, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-issue-degrade", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 204, title: "Plain issue", state: "open", user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); // no closeIssue primitive — degrades to hold + expect(seen.comments.some((c) => c.includes("cooldown limit"))).toBe(true); + }); + + 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(205, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-exempt", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 205, title: "Exempt author", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); + const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); + expect(applied?.n).toBe(0); + }); + + 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 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); + for (let i = 0; i < 5; i += 1) { + await processJob(env, { + type: "github-webhook", + deliveryId: `nag-third-party-${i}`, + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 206, title: "Third party pinger", state: "open", pull_request: {}, user: { login: "pr-author" }, author_association: "NONE" }, + comment: { id: i, body: "@gittensory help", user: { login: "bystander", type: "User" }, author_association: "NONE" }, + }, + }); + } + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(0); // never even tracked — the commenter is not the thread's own author + expect(seen.closed).toBe(false); + }); + + it("no-op owner-exemption when repoFullName has no slash (repoOwner is empty — never wrongly matches the commenter)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + 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 }); + 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(209, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-noslash", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }, + issue: { number: 209, title: "Slash-free repo", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + // repoOwner="" (branch false) → commenter "chatty" never equals "" → the owner-exemption is skipped and + // the throttle still engages normally (the comment post itself can't succeed for a slash-free repo — no + // owner/repo to target — but that failure is swallowed by design, same as every other best-effort notice + // in this file). Proven by reaching + completing the hold branch without the handler crashing. + const applied = await env.DB.prepare("select outcome from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ outcome: string }>(); + expect(applied?.outcome).toBe("completed"); + }); + + 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 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); + for (let i = 0; i < 3; i += 1) { + await processJob(env, { + type: "github-webhook", + deliveryId: `nag-owner-${i}`, + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 207, title: "Owner PR", state: "open", pull_request: {}, user: { login: "JSONbored" }, author_association: "OWNER" }, + comment: { id: i, body: "@gittensory help", user: { login: "JSONbored", type: "User" }, author_association: "OWNER" }, + }, + }); + } + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(0); + expect(seen.closed).toBe(false); + }); + + 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(208, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-admin", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 208, title: "Admin PR", state: "open", pull_request: {}, user: { login: "fleet-admin" }, author_association: "NONE" }, + comment: { id: 6, body: "@gittensory help", user: { login: "fleet-admin", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); + const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); + expect(applied?.n).toBe(0); + }); + + 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(209, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-hold-dryrun", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 209, title: "Dry-run hold", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.comments).toHaveLength(0); // dry-run — no live comment posted + const applied = await env.DB.prepare("select outcome from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ outcome: string }>(); + expect(applied?.outcome).toBe("denied"); + }); + + 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(210, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-already-closed", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 210, title: "Already closed", state: "closed", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); + const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); + expect(applied?.n).toBe(0); // fell through silently — nothing left to act on + }); + + 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(211, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-observe-only", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 211, title: "Observe-only autonomy", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); + const applied = await env.DB.prepare("select outcome, detail from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ outcome: string; detail: string }>(); + expect(applied?.outcome).toBe("denied"); + expect(applied?.detail).toContain("autonomy is not acting"); + }); + + 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubReviewNagFetch(212, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "nag-no-installation", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 212, title: "No installation row", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); // no installation permissions on record — the write-permission gate denies it + const closeAudit = await env.DB.prepare("select outcome from audit_events where event_type = 'agent.action.close'").first<{ outcome: string }>(); + expect(closeAudit?.outcome).toBe("denied"); + }); + }); + + describe("maintainer-mention nag moderation (#label-scoping)", () => { + function stubMonitoredMentionFetch(prNumber: number, seen: { comments: string[]; labels: string[]; closed: boolean }) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); + if (url.endsWith(`/pulls/${prNumber}`) && method === "PATCH") { + seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; + return Response.json({ number: prNumber, state: "closed" }); + } + if (url.endsWith(`/pulls/${prNumber}`)) return Response.json({ number: prNumber, state: "open", head: { sha: `sha${prNumber}` }, mergeable_state: "clean" }); + if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { + seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); + if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { + seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); + return Response.json({ id: seen.comments.length }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + } + + 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 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); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-off-default", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 300, title: "No monitored logins", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@JSONbored are you going to review this?", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(0); + }); + + 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 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); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-under-threshold", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 301, title: "Under threshold", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "Hey @JSONbored can you take a look?", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(1); + expect(seen.closed).toBe(false); + }); + + 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 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); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-bot-shaped-literal", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 312, title: "Bot mention", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "Please check this @dependabot[bot].", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(1); + }); + + 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 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); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-bot-shaped-false-positive", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 313, title: "Bot false positive", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "This mentions @dependabotb, not the bot actor.", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(0); + }); + + 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 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); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-case-insensitive", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 302, title: "Case + unrelated", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@jsonbored please review", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(1); // case-insensitive match on the configured "JSONbored" + + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-unrelated", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 302, title: "Case + unrelated", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 2, body: "this uses @some-other-package internally", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const pingsAfter = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(pingsAfter?.n).toBe(1); // unrelated mention did not add a ping + }); + + it("counts a monitored-login mention independently of the @gittensory 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 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); + // A comment mentioning BOTH @gittensory and the monitored login should tick both counters independently. + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-both", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 303, title: "Independent counters", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@gittensory help — also @JSONbored can you look?", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + const gittensoryPings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); + const mentionPings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(gittensoryPings?.n).toBe(1); + expect(mentionPings?.n).toBe(1); + }); + + 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubMonitoredMentionFetch(304, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-hold", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 304, title: "Hold on mention", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@JSONbored please look at this", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); + expect(seen.comments.some((c) => c.includes("cooldown limit for @JSONbored"))).toBe(true); + expect(seen.comments).toHaveLength(1); // short-circuited — no normal answer-card reply + }); + + it("close policy on a PR thread: labels + closes once the threshold is crossed, reusing reviewNagLabel", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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 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" }); + } + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubMonitoredMentionFetch(305, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-close", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 305, title: "Close on mention", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@JSONbored please look at this", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(true); + expect(seen.labels).toContain("too-chatty"); + // #label-scoping: close: "auto" alone (no broad label: "auto") is sufficient for the label AND the close. + }); + + it("REGRESSION (#review-nag-cross-pr-carryover): a contributor who exhausted their @-mention pings for ONE login on PR A carries that login's count over to a BRAND-NEW PR B", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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" } }); + // 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) { + await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.monitored_mention_ping", actor: "chatty", targetKey: "JSONbored/gittensory#320#mention:jsonbored", outcome: "completed" }); + } + // PR B: a BRAND-NEW PR from the SAME contributor mentioning the SAME login. A new issue.number is a new + // targetKey the old per-target count would treat as a clean slate. Only ONE mention is sent here. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 321, title: "PR B (brand new)", state: "open", user: { login: "chatty" }, head: { sha: "sha321" }, author_association: "NONE", labels: [], body: "" }); + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubMonitoredMentionFetch(321, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-carryover-pr-b", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 321, title: "PR B (brand new)", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@JSONbored please look at this", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + // Under the OLD per-targetKey count, this is mention-ping 1/3 on PR B alone -- under threshold, no action. + // The FIX counts every prior @JSONbored mention-ping across the whole repo, so this single PR-B ping is + // already #4 overall (3 carried over from PR A + this one), crossing maxPings=3 on the very first ping. + expect(seen.closed).toBe(true); + const prA = await env.DB.prepare("select state from pull_requests where number = 320").first<{ state: string }>(); + expect(prA?.state).toBe("closed"); // PR A is untouched by this second evaluation + }); + + it("REGRESSION (#review-nag-cross-pr-carryover): a DIFFERENT monitored login mentioned on PR B keeps its own independent budget, unaffected by another login's exhausted count", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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" }, + }); + // 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) { + await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.monitored_mention_ping", actor: "chatty", targetKey: "JSONbored/gittensory#322#mention:jsonbored", outcome: "completed" }); + } + // PR B: the SAME contributor mentions a DIFFERENT monitored login ("other-maintainer") for the FIRST time. + // If the repo-wide carryover fix accidentally merged every mentioned login into one shared count, this + // single ping would incorrectly already be "#4" and get throttled -- it must instead be a fresh 1/3. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 323, title: "PR B (different login)", state: "open", user: { login: "chatty" }, head: { sha: "sha323" }, author_association: "NONE", labels: [], body: "" }); + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubMonitoredMentionFetch(323, seen); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-independent-login-pr-b", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 323, title: "PR B (different login)", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@other-maintainer could you take a look?", user: { login: "chatty", type: "User" }, author_association: "NONE" }, + }, + }); + expect(seen.closed).toBe(false); // "other-maintainer"'s own budget is untouched by @JSONbored's exhausted count + const mentionPings = await env.DB.prepare( + "select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping' and target_key = 'JSONbored/gittensory#323#mention:other-maintainer'", + ).first<{ n: number }>(); + expect(mentionPings?.n).toBe(1); // recorded as ping 1/3 for THIS login, not folded into @JSONbored's tally + }); + + 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"] }); + const seen = { comments: [] as string[], labels: [] as string[], closed: false }; + stubMonitoredMentionFetch(306, seen); + for (const [commenter, prNumber] of [ + ["JSONbored", 306], // repo owner + ["fleet-admin", 307], // admin login + ["some-bot[bot]", 308], // automation bot + ["trusted-regular", 309], // configured exemption + ] as const) { + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: prNumber, title: "Exempt", state: "open", user: { login: commenter }, author_association: "NONE", labels: [], body: "" }); + await processJob(env, { + type: "github-webhook", + deliveryId: `mention-exempt-${prNumber}`, + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: prNumber, title: "Exempt", state: "open", pull_request: {}, user: { login: commenter }, author_association: "NONE" }, + comment: { id: prNumber, body: "@JSONbored can you review?", user: { login: commenter, type: commenter.endsWith("[bot]") ? "Bot" : "User" }, author_association: "NONE" }, + }, + }); + } + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(0); + }); + + 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 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); + await processJob(env, { + type: "github-webhook", + deliveryId: "mention-third-party", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 310, title: "Third party", state: "open", pull_request: {}, user: { login: "thread-author" }, author_association: "NONE" }, + comment: { id: 1, body: "@JSONbored can you weigh in here?", user: { login: "a-different-commenter", type: "User" }, author_association: "NONE" }, + }, + }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + expect(pings?.n).toBe(0); + }); + + 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 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); + const payload = { + action: "created" as const, + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" as const } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 311, title: "Redelivery", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, + comment: { id: 1, body: "@JSONbored ping", user: { login: "chatty", type: "User" as const }, author_association: "NONE" }, + }; + await processJob(env, { type: "github-webhook", deliveryId: "mention-redelivery-same", eventName: "issue_comment", payload }); + await processJob(env, { type: "github-webhook", deliveryId: "mention-redelivery-same", eventName: "issue_comment", payload }); + const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); + // NOTE: unlike #2560's per-command limiter, review-nag/monitored-mention ping recording does not itself + // dedup by deliveryId -- it always records. This assertion documents CURRENT behavior (2 pings from 2 + // deliveries) rather than asserting an idempotency guarantee this handler does not provide. + expect(pings?.n).toBe(2); + }); + }); + + describe("per-command @gittensory rate limit (#2560)", () => { + function stubCommandRateLimitFetch(issueNumber: number, seen: { comments: string[] }) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); + if (url.includes(`/issues/${issueNumber}/comments`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${issueNumber}/comments`) && method === "POST") { + seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); + return Response.json({ id: seen.comments.length }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + } + + function mentionPayload(issueNumber: number, body: string) { + return { + action: "created" as const, + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: issueNumber, title: "Rate limit target", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { id: 1, body, user: { login: "maintainer", type: "User" }, author_association: "OWNER" }, + }; + } + + it("is off by default — no invocation is tracked and every command dispatches normally", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 300, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); + const seen = { comments: [] as string[] }; + stubCommandRateLimitFetch(300, seen); + for (let i = 0; i < 25; i += 1) { + await processJob(env, { type: "github-webhook", deliveryId: `rl-off-${i}`, eventName: "issue_comment", payload: mentionPayload(300, "@gittensory help") }); + } + const invocations = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_invocation'").first<{ n: number }>(); + expect(invocations?.n).toBe(0); + expect(seen.comments).toHaveLength(25); // every one of the 25 invocations dispatched normally + }); + + 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 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); + await processJob(env, { type: "github-webhook", deliveryId: "rl-under", eventName: "issue_comment", payload: mentionPayload(301, "@gittensory help") }); + const invocations = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_invocation'").first<{ n: number }>(); + expect(invocations?.n).toBe(1); // 1st of 5 allowed + const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ n: number }>(); + expect(applied?.n).toBe(0); // under threshold — no hold + expect(seen.comments).toHaveLength(1); // the normal answer card still posted + }); + + 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 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" }); + } + const seen = { comments: [] as string[] }; + stubCommandRateLimitFetch(302, seen); + await processJob(env, { type: "github-webhook", deliveryId: "rl-cheap-over", eventName: "issue_comment", payload: mentionPayload(302, "@gittensory help") }); + // Only ONE comment posted — the short-circuit skipped the normal answer-card dispatch. + expect(seen.comments).toHaveLength(1); + expect(seen.comments[0]).toContain("rate limit"); + const applied = await env.DB.prepare("select outcome, detail from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ outcome: string; detail: string }>(); + expect(applied?.outcome).toBe("completed"); + expect(applied?.detail).toContain("hold applied"); + }); + + 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 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" }); + } + const seen = { comments: [] as string[] }; + stubCommandRateLimitFetch(303, seen); + await processJob(env, { type: "github-webhook", deliveryId: "rl-ai-over", eventName: "issue_comment", payload: mentionPayload(303, "@gittensory next-action") }); + expect(seen.comments).toHaveLength(1); + expect(seen.comments[0]).toContain("rate limit"); + const applied = await env.DB.prepare("select detail from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ detail: string }>(); + expect(applied?.detail).toContain("limit 2"); // the AI limit (2), not the cheap default (20) + }); + + 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 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" }); + const seen = { comments: [] as string[] }; + stubCommandRateLimitFetch(304, seen); + // A DIFFERENT command ("miner-context") on the same thread by the same actor must not be affected. + await processJob(env, { type: "github-webhook", deliveryId: "rl-independent", eventName: "issue_comment", payload: mentionPayload(304, "@gittensory miner-context") }); + expect(seen.comments).toHaveLength(1); + expect(seen.comments[0]).not.toContain("rate limit"); + const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ n: number }>(); + expect(applied?.n).toBe(0); + }); + + 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 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[] }; + stubCommandRateLimitFetch(305, seen); + await processJob(env, { type: "github-webhook", deliveryId: "rl-dry-run", eventName: "issue_comment", payload: mentionPayload(305, "@gittensory help") }); + expect(seen.comments).toHaveLength(0); // held, but dry-run posts nothing live + const applied = await env.DB.prepare("select outcome from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ outcome: string }>(); + expect(applied?.outcome).toBe("denied"); + }); + + it("REGRESSION: a redelivered webhook (same deliveryId) does not double-count — the replay is a no-op, not a second invocation", async () => { + // GitHub can and does redeliver the same issue_comment event (timeout/retry). Before the fix, the + // 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 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); + // The SAME deliveryId, redelivered — GitHub's own retry behavior on a timeout/5xx. + await processJob(env, { type: "github-webhook", deliveryId: "rl-redelivered", eventName: "issue_comment", payload: mentionPayload(306, "@gittensory help") }); + await processJob(env, { type: "github-webhook", deliveryId: "rl-redelivered", eventName: "issue_comment", payload: mentionPayload(306, "@gittensory help") }); + + const invocations = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_invocation'").first<{ n: number }>(); + expect(invocations?.n).toBe(1); // only ONE invocation recorded despite two processing passes + expect(seen.comments).toHaveLength(1); // the replay is suppressed entirely — no second answer card + expect(seen.comments.every((c) => !c.includes("rate limit"))).toBe(true); + const suppressed = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_redelivery_suppressed'").first<{ n: number }>(); + expect(suppressed?.n).toBe(1); + }); + }); + + it("denies a maintainer Q&A command from an org member without real repo permission (#788)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 96, + title: "Org member tries a maintainer command", + state: "open", + user: { login: "alice" }, + author_association: "NONE", + labels: [], + body: "", + }); + const calls = { comments: 0, permission: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // The commenter is an org MEMBER but has only READ access to THIS repo — not a maintainer/collaborator. + if (url.includes("/collaborators/orgmember/permission")) { + calls.permission += 1; + return Response.json({ permission: "read" }); + } + if (url.includes("/issues/") && url.includes("/comments")) { + calls.comments += 1; + return Response.json([]); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-org-member-no-permission", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 96, title: "Org member tries a maintainer command", state: "open", pull_request: {}, user: { login: "alice" }, author_association: "NONE" }, + // author_association MEMBER would have granted the maintainer role pre-#788; it no longer does. + comment: { id: 96, body: "@gittensory queue-summary", user: { login: "orgmember", type: "User" }, author_association: "MEMBER" }, + }, + }); + expect(calls.permission).toBe(1); // the REAL repo permission was consulted, not the spoofable association + expect(calls.comments).toBe(0); // …and the org member was denied — no maintainer reply + const skip = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.agent_command_skipped", "JSONbored/gittensory#96") + .first<{ detail: string }>(); + expect(skip?.detail).toBe("not_maintainer_or_pr_author"); + }); + + it("records command usage as an error when miner authorization cannot be checked", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return new Response("api down", { status: 503 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-miner-unavailable", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 84, title: "Miner unavailable PR", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { id: 5, body: "@gittensory preflight", user: { login: "oktofeesh1", type: "User" }, author_association: "NONE" }, + }, + }); + + const usageEvents = await listProductUsageEvents(env, { limit: 5 }); + expect(usageEvents).toEqual([ + expect.objectContaining({ surface: "github_app", eventName: "agent_command_skipped", outcome: "error", metadata: expect.objectContaining({ reason: "miner_detection_unavailable" }) }), + ]); + }); + + it("does not let product usage write failures block GitHub command audits", async () => { + const env = withProductUsageInsertFailure(createTestEnv()); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-product-usage-down", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 90, title: "Plain issue", state: "open", user: { login: "reporter" } }, + comment: { id: 1, body: "@gittensory preflight", user: { login: "reporter", type: "User" }, author_association: "NONE" }, + }, + }); + + const audit = await env.DB.prepare("select event_type, detail from audit_events where target_key = ?") + .bind("JSONbored/gittensory#90") + .all<{ event_type: string; detail: string }>(); + expect(audit.results).toEqual([expect.objectContaining({ event_type: "github_app.agent_command_skipped", detail: "not_a_pull_request_thread" })]); + }); + + it("audits command authorization errors when miner detection is unavailable", async () => { + const env = createTestEnv(); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString() === "https://api.gittensor.io/miners") return new Response("unavailable", { status: 503 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-miner-unavailable", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 84, title: "Unavailable miner check", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { id: 5, body: "@gittensory preflight", user: { login: "oktofeesh1", type: "User" }, author_association: "NONE" }, + }, + }); + + const event = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.agent_command_skipped", "JSONbored/gittensory#84") + .first<{ outcome: string; detail: string }>(); + expect(event).toMatchObject({ outcome: "error", detail: "miner_detection_unavailable" }); + }); + + it("detects a changes-requested review notification for the PR author", async () => { + const enqueued: Array<{ type: string }> = []; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + JOBS: { + async send(message: { type: string }) { + enqueued.push(message); + }, + } as unknown as Queue, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/repos/JSONbored/gittensory/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "review-changes-requested", + eventName: "pull_request_review", + payload: { + action: "submitted", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 42, + title: "Add feature", + state: "open", + user: { login: "contributor", type: "User" }, + html_url: "https://github.com/JSONbored/gittensory/pull/42", + }, + review: { + state: "changes_requested", + user: { login: "maintainer", type: "User" }, + submitted_at: "2026-05-28T12:00:00.000Z", + html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", + }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const detected = await env.DB.prepare("select actor, target_key, outcome, detail, metadata_json from audit_events where event_type = ?") + .bind("notification.event_detected") + .all<{ actor: string; target_key: string; outcome: string; detail: string; metadata_json: string }>(); + expect(detected.results).toHaveLength(1); + expect(detected.results[0]).toMatchObject({ + actor: "maintainer", + target_key: "contributor", + outcome: "success", + detail: "pull_request_changes_requested for JSONbored/gittensory#42", + }); + expect(JSON.parse(detected.results[0]!.metadata_json)).toMatchObject({ + deliveryId: "review-changes-requested", + eventType: "pull_request_changes_requested", + recipientLogin: "contributor", + repoFullName: "JSONbored/gittensory", + pullNumber: 42, + dedupKey: "changes_requested:JSONbored/gittensory#42:maintainer:2026-05-28T12:00:00.000Z", + }); + expect(JSON.stringify(detected.results[0])).not.toMatch(/trust score|wallet|hotkey|reward estimate|reviewability/i); + + const evaluateJob = enqueued.find((message): message is { type: "notify-evaluate"; events: Array<{ recipientLogin: string }> } => message.type === "notify-evaluate"); + expect(evaluateJob).toBeDefined(); + expect(evaluateJob!.events).toHaveLength(1); + expect(evaluateJob!.events[0]!.recipientLogin).toBe("contributor"); + }); + + it("skips changes-requested review notifications from reviewers without repository write permission", async () => { + const enqueued: Array<{ type: string }> = []; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + JOBS: { + async send(message: { type: string }) { + enqueued.push(message); + }, + } as unknown as Queue, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/repos/JSONbored/gittensory/collaborators/drive-by-user/permission")) return Response.json({ permission: "read" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "review-changes-requested-low-priv", + eventName: "pull_request_review", + payload: { + action: "submitted", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 42, + title: "Add feature", + state: "open", + user: { login: "contributor", type: "User" }, + html_url: "https://github.com/JSONbored/gittensory/pull/42", + }, + review: { + state: "changes_requested", + user: { login: "drive-by-user", type: "User" }, + submitted_at: "2026-05-28T12:00:00.000Z", + html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", + }, + sender: { login: "drive-by-user", type: "User" }, + }, + }); + + const detected = await env.DB.prepare("select actor from audit_events where event_type = ?") + .bind("notification.event_detected") + .all<{ actor: string }>(); + expect(detected.results).toEqual([]); + expect(enqueued).not.toContainEqual(expect.objectContaining({ type: "notify-evaluate" })); + }); + + it("skips changes-requested review notifications with an unknown actor without consulting repo permissions", async () => { + const enqueued: Array<{ type: string }> = []; + const permissionCalls: string[] = []; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + JOBS: { + async send(message: { type: string }) { + enqueued.push(message); + }, + } as unknown as Queue, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/")) { + permissionCalls.push(url); + return Response.json({ permission: "admin" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "review-changes-requested-unknown-actor", + eventName: "pull_request_review", + payload: { + action: "submitted", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 42, + title: "Add feature", + state: "open", + user: { login: "contributor", type: "User" }, + html_url: "https://github.com/JSONbored/gittensory/pull/42", + }, + // Neither the review nor the sender carries a login → detectNotificationEvents emits actorLogin "unknown". + review: { + state: "changes_requested", + submitted_at: "2026-05-28T12:00:00.000Z", + html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", + }, + }, + }); + + const detected = await env.DB.prepare("select actor from audit_events where event_type = ?") + .bind("notification.event_detected") + .all<{ actor: string }>(); + expect(detected.results).toEqual([]); + expect(enqueued).not.toContainEqual(expect.objectContaining({ type: "notify-evaluate" })); + // The unknown-actor guard short-circuits before any collaborator-permission lookup. + expect(permissionCalls).toEqual([]); + }); + + it("skips changes-requested review notifications when the webhook has no installation", async () => { + const enqueued: Array<{ type: string }> = []; + const permissionCalls: string[] = []; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + JOBS: { + async send(message: { type: string }) { + enqueued.push(message); + }, + } as unknown as Queue, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/")) { + permissionCalls.push(url); + return Response.json({ permission: "admin" }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "review-changes-requested-no-installation", + eventName: "pull_request_review", + payload: { + action: "submitted", + // No installation present → installationId is undefined and the reviewer cannot be verified. + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 42, + title: "Add feature", + state: "open", + user: { login: "contributor", type: "User" }, + html_url: "https://github.com/JSONbored/gittensory/pull/42", + }, + review: { + state: "changes_requested", + user: { login: "maintainer", type: "User" }, + submitted_at: "2026-05-28T12:00:00.000Z", + html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", + }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const detected = await env.DB.prepare("select actor from audit_events where event_type = ?") + .bind("notification.event_detected") + .all<{ actor: string }>(); + expect(detected.results).toEqual([]); + expect(enqueued).not.toContainEqual(expect.objectContaining({ type: "notify-evaluate" })); + // With no installation we cannot verify the reviewer, so no permission lookup is attempted. + expect(permissionCalls).toEqual([]); + }); + + it.each(["submitted", "dismissed", "edited"] as const)( + "bumps reviewsInvalidatedAt for the right repo+PR on a pull_request_review '%s' webhook (#2537)", + async (action) => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + JOBS: { async send() {} } as unknown as Queue, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + // Seed an existing sync-state row so the assertion can confirm ONLY reviewsInvalidatedAt moved. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 42, + title: "Add feature", + state: "open", + user: { login: "contributor" }, + head: { sha: "sha-42" }, + labels: [], + body: "", + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: `review-invalidate-${action}`, + eventName: "pull_request_review", + payload: { + action, + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 42, + title: "Add feature", + state: "open", + user: { login: "contributor", type: "User" }, + html_url: "https://github.com/JSONbored/gittensory/pull/42", + }, + review: { + state: action === "dismissed" ? "DISMISSED" : "APPROVED", + user: { login: "maintainer", type: "User" }, + submitted_at: "2026-05-28T12:00:00.000Z", + html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", + }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const state = await getPullRequestDetailSyncState(env, "JSONbored/gittensory", 42); + expect(state?.reviewsInvalidatedAt).toBeTruthy(); + }, + ); + + it("does not bump reviewsInvalidatedAt for a pull_request_review action outside submitted/dismissed/edited", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + JOBS: { async send() {} } as unknown as Queue, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "review-invalidate-unsupported-action", + eventName: "pull_request_review", + payload: { + // "submitted" | "dismissed" | "edited" are the only invalidating actions; GitHub also emits others + // (e.g. review comments carry their own event) that must NOT stamp the cache marker. + action: "unrecognized_action" as unknown as "submitted", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 43, + title: "Add feature", + state: "open", + user: { login: "contributor", type: "User" }, + html_url: "https://github.com/JSONbored/gittensory/pull/43", + }, + review: { + state: "APPROVED", + user: { login: "maintainer", type: "User" }, + submitted_at: "2026-05-28T12:00:00.000Z", + html_url: "https://github.com/JSONbored/gittensory/pull/43#pullrequestreview-1", + }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(await getPullRequestDetailSyncState(env, "JSONbored/gittensory", 43)).toBeNull(); + }); + + it("notifies issue-watchers when a new grabbable maintainer-created issue opens (#699 path B)", async () => { + const enqueued: Array<{ type: string; events?: Array<{ eventType: string; recipientLogin: string; pullNumber: number }> }> = []; + const env = createTestEnv({ JOBS: { async send(message: { type: string }) { enqueued.push(message); } } as unknown as Queue }); + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest + const watcherLogins = Array.from({ length: 205 }, (_, index) => `watcher-${String(index + 1).padStart(3, "0")}`); + for (const login of watcherLogins) { + await upsertIssueWatchSubscription(env, { login, repoFullName: "JSONbored/gittensory" }); + } + await upsertIssueWatchSubscription(env, { login: "maintainer", repoFullName: "JSONbored/gittensory" }); // the author — should be skipped + + await processJob(env, { + type: "github-webhook", + deliveryId: "issue-watch-open", + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 91, title: "Add caching to the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }, + }, + }); + + // Batched but bounded (#selfhost-maintenance-self-pin): watcher matches from this ONE webhook delivery ride in + // chunked notify-evaluate jobs, not one job per watcher and not one unbounded queue payload. + const evaluateJobs = enqueued.filter((m): m is { type: "notify-evaluate"; events: Array<{ eventType: string; recipientLogin: string; pullNumber: number }> } => m.type === "notify-evaluate"); + expect(evaluateJobs.map((job) => job.events).map((events) => events.length)).toEqual([100, 100, 5]); + const watchEvents = evaluateJobs.flatMap((job) => job.events).filter((event) => event.eventType === "issue_watch_match"); + expect(watchEvents.map((event) => event.recipientLogin).sort()).toEqual(watcherLogins); // maintainer (author) skipped + expect(watchEvents.every((event) => event.pullNumber === 91)).toBe(true); + + const detected = await env.DB.prepare("select metadata_json from audit_events where event_type = 'notification.event_detected' and target_key = ?").bind("watcher-001").first<{ metadata_json: string }>(); + expect(JSON.parse(detected!.metadata_json)).toMatchObject({ eventType: "issue_watch_match", recipientLogin: "watcher-001", repoFullName: "JSONbored/gittensory" }); + }); + + it("REGRESSION (#3218 review): chunk membership across a >100-watcher batch is order-independent -- the SAME watcher set in a different arrival order still produces the SAME set of chunk coalesce keys", async () => { + const watcherLogins = Array.from({ length: 205 }, (_, index) => `watcher-${String(index + 1).padStart(3, "0")}`); + + const enqueueNotifyEvaluateJobs = async (loginOrder: string[]): Promise }>> => { + const enqueued: Array<{ type: string; events?: Array<{ dedupKey: string }> }> = []; + const env = createTestEnv({ JOBS: { async send(message: { type: string }) { enqueued.push(message); } } as unknown as Queue }); + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest + // listIssueWatchersForRepo has no ORDER BY -- insertion order IS read-back order, so inserting in a + // different order here genuinely reproduces two logically-identical detection passes disagreeing on + // notificationEvents' arrival order, exactly the redelivery scenario the review is concerned about. + for (const login of loginOrder) { + await upsertIssueWatchSubscription(env, { login, repoFullName: "JSONbored/gittensory" }); + } + await processJob(env, { + type: "github-webhook", + deliveryId: `issue-watch-open-${loginOrder[0]}`, + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 91, title: "Add caching to the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }, + }, + }); + vi.unstubAllGlobals(); + return enqueued.filter((m): m is { type: "notify-evaluate"; events: Array<{ dedupKey: string }> } => m.type === "notify-evaluate"); + }; + + const coalesceKeysFor = (jobs: Array<{ type: string; events: Array<{ dedupKey: string }> }>): Array => + jobs.map((job) => jobCoalesceKey(JSON.stringify(job))).sort(); + + const forwardJobs = await enqueueNotifyEvaluateJobs(watcherLogins); + const reversedJobs = await enqueueNotifyEvaluateJobs([...watcherLogins].reverse()); + + // Same chunk SIZES either way (chunking itself is unaffected -- only membership was the risk). + expect(forwardJobs.map((job) => job.events.length)).toEqual([100, 100, 5]); + expect(reversedJobs.map((job) => job.events.length)).toEqual([100, 100, 5]); + // The set of chunk-level coalesce keys must match -- proving a redelivery whose events resolve in a + // different order still coalesces with the original batch instead of silently re-running as "new" work. + expect(coalesceKeysFor(reversedJobs)).toEqual(coalesceKeysFor(forwardJobs)); + }); + + it("appends issue-side slop findings to the issue advisory only when slop is opted in (#533)", async () => { + const env = createTestEnv(); + vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositoryFromGitHub(env, { name: "other", full_name: "JSONbored/other", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", slopGateMode: "advisory" }); + // JSONbored/other keeps the default slopGateMode "off". + + const emptyBodyIssue = (repoFull: string, name: string, number: number) => ({ + type: "github-webhook" as const, + deliveryId: `issue-slop-${number}`, + eventName: "issues", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name, full_name: repoFull, private: false, owner: { login: "JSONbored" } }, + issue: { number, title: "Something is broken", state: "open", user: { login: "reporter" }, body: " " }, + }, + }); + await processJob(env, emptyBodyIssue("JSONbored/gittensory", "gittensory", 501)); + await processJob(env, emptyBodyIssue("JSONbored/other", "other", 502)); + + const slopOn = await env.DB.prepare("select findings_json from advisories where target_type = 'issue' and repo_full_name = ?").bind("JSONbored/gittensory").first<{ findings_json: string }>(); + const slopOff = await env.DB.prepare("select findings_json from advisories where target_type = 'issue' and repo_full_name = ?").bind("JSONbored/other").first<{ findings_json: string }>(); + expect(slopOn?.findings_json).toContain("empty_issue_body"); // opted in → triage finding present + expect(slopOff?.findings_json ?? "").not.toContain("empty_issue_body"); // default off → no slop finding + }); + + it("clears the persisted dashboard slop score when the slop gate is off (#911)", async () => { + // Merge-readiness still collects the live slop score, so shouldCollectSlopEvidence runs even with the + // slop gate disabled — but with slopGateMode "off" the persisted dashboard row must be cleared to null + // so a previously cached score doesn't linger after a maintainer disables slop. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + slopGateMode: "off", // dashboard slop disabled… + mergeReadinessGateMode: "advisory", // …but readiness keeps the live score in play + }); + // Seed the PR row plus a stale dashboard slop score that the slop-off pass must clear. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 91, + title: "Add helper", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "slopoff123" }, + labels: [], + body: "Adds a helper.", + }); + await updatePullRequestSlopAssessment(env, "JSONbored/gittensory", 91, { slopRisk: 80, slopBand: "high" }); + await upsertPullRequestFile(env, { + repoFullName: "JSONbored/gittensory", + pullNumber: 91, + path: "src/helper.ts", + status: "modified", + additions: 5, + deletions: 0, + changes: 5, + payload: {}, + }); + expect((await getPullRequest(env, "JSONbored/gittensory", 91))?.slopRisk).toBe(80); // stale score present pre-run + + const refreshedFiles: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/91/files")) { + refreshedFiles.push(url); + return Response.json([{ filename: "src/helper.ts", status: "modified", additions: 5, deletions: 0, changes: 5 }]); + } + if (url.includes("/commits/slopoff123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs")) return Response.json({ id: 991 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "slop-off-clear", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 91, title: "Add helper", state: "open", user: { login: "contributor" }, head: { sha: "slopoff123" }, labels: [], body: "Adds a helper." }, + }, + }); + + // Slop gate off → the previously persisted dashboard score is null-persisted, not left stale. + const cleared = await getPullRequest(env, "JSONbored/gittensory", 91); + expect(cleared?.slopRisk).toBeNull(); + expect(cleared?.slopBand).toBeNull(); + expect(refreshedFiles).toHaveLength(1); + }); + + it("#dup-winner: flag ON spares the lowest open sibling — no duplicate block, slop not penalized for the cluster", async () => { + // GITTENSORY_DUPLICATE_WINNER ON. A same-issue cluster of OPEN PRs (#91 winner, #92 loser) under + // duplicatePrGateMode: block. The winner (#91, lowest open number) must NOT be gate-blocked or slop- + // penalized as a duplicate — it is judged on its own merits. This drives the flag-ON branch of the + // processors gate path (isDupWinner) + the advisory duplicate-finding suppression. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_DUPLICATE_WINNER: "true" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + duplicatePrGateMode: "block", + slopGateMode: "advisory", + qualityGateMode: "block", + qualityGateMinScore: 95, + }); + // The shared issue + the HIGHER-numbered open sibling (#92) → forms the same-issue duplicate cluster. + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "Cache the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 91, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win91" }, labels: [], body: "Fixes #1\n\nValidation: npm test" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 92, title: "Also fix the cache", state: "open", user: { login: "other" }, author_association: "CONTRIBUTOR", head: { sha: "sib92" }, labels: [], body: "Fixes #1" }); + + let gatePatchBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/91/files")) return Response.json([{ filename: "src/cache.ts", status: "modified", additions: 12, deletions: 0, changes: 12 }]); + if (url.includes("/commits/win91/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "PATCH") { + gatePatchBody = JSON.parse(String(init?.body ?? "{}")) as typeof gatePatchBody; + return Response.json({ id: 960 }); + } + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 960 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "dup-winner-on", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 91, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win91" }, labels: [], body: "Fixes #1\n\nValidation: npm test" }, + }, + }); + + // Winner survives: a later duplicate sibling must not lower readiness below a blocking threshold. + expect(gatePatchBody.conclusion).not.toBe("failure"); + expect(gatePatchBody.output?.text ?? "").not.toContain("readiness_score_below_threshold"); + // The persisted advisory for the winner OMITS the duplicate finding (suppressed) — that is what suppresses + // the gate failure and the auto-close duplicate cause. + const winnerAdvisory = await env.DB.prepare("select findings_json from advisories where target_type = 'pull_request' and repo_full_name = ? and pull_number = ?").bind("JSONbored/gittensory", 91).first<{ findings_json: string }>(); + expect(winnerAdvisory?.findings_json ?? "").not.toContain("duplicate_pr_risk"); + }); + + it("#dup-winner: flag OFF keeps every same-issue sibling blocked (byte-identical) — the winner is also closed-eligible", async () => { + // Same cluster, flag OFF (default). The lowest open PR (#91) STILL gets the duplicate block + finding, + // exactly like today — no winner is spared. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + duplicatePrGateMode: "block", + slopGateMode: "advisory", + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "Cache the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 92, title: "Also fix the cache", state: "open", user: { login: "other" }, author_association: "CONTRIBUTOR", head: { sha: "sib92b" }, labels: [], body: "Fixes #1" }); + + let gatePatchBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/91/files")) return Response.json([{ filename: "src/cache.ts", status: "modified", additions: 12, deletions: 0, changes: 12 }]); + if (url.includes("/commits/win91b/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "PATCH") { + gatePatchBody = JSON.parse(String(init?.body ?? "{}")) as typeof gatePatchBody; + return Response.json({ id: 961 }); + } + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 961 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "dup-winner-off", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 91, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win91b" }, labels: [], body: "Fixes #1" }, + }, + }); + + // Flag OFF: the duplicate block still fires for the lowest sibling — the Gate fails, the finding persists. + expect(gatePatchBody.conclusion).toBe("failure"); + const winnerAdvisory = await env.DB.prepare("select findings_json from advisories where target_type = 'pull_request' and repo_full_name = ? and pull_number = ?").bind("JSONbored/gittensory", 91).first<{ findings_json: string }>(); + expect(winnerAdvisory?.findings_json ?? "").toContain("duplicate_pr_risk"); + }); + + it("REGRESSION (#dup-winner-slop-drift): maybePublishPrPublicSurface's slop penalty uses the LIVE-reconciled siblings, not a raw stale-cached read — a stale-cached-open lower sibling that is actually CLOSED on GitHub must not deny this PR winner status / slop-penalize it for the cluster", async () => { + // GITTENSORY_DUPLICATE_WINNER ON. PR #95 (this PR, being reviewed) links issue #1; PR #90 (LOWER-numbered, + // same linked issue) is cached `open` in the DB (a missed/delayed `closed` webhook), but GitHub's LIVE state + // for #90 is actually `closed`. Before the fix, maybePublishPrPublicSurface's own duplicate-winner election + // read the raw, un-reconciled `listPullRequests` result (still showing #90 as open) and so wrongly denied + // #95 winner status, applying the duplicateClusterMembership slop penalty (weight 15, persisted slop_band + // "low") even though the gate's OWN reconciled otherOpenPullRequests (used to build the advisory/gate + // disposition) had already correctly dropped #90. After the fix, both paths agree: #95 is the winner (no + // open siblings once reconciled) and carries NO duplicate-cluster slop penalty (slop_band "clean"). + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_DUPLICATE_WINNER: "true" }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + duplicatePrGateMode: "block", + slopGateMode: "advisory", + }); + await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "Cache the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }); + // Stale-cached-open sibling: the DB still says #90 is open (the closed webhook was missed/delayed). + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 90, title: "Older attempt at the cache fix", state: "open", user: { login: "other" }, author_association: "CONTRIBUTOR", head: { sha: "sib90" }, labels: [], body: "Fixes #1" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 95, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win95" }, labels: [], body: "Fixes #1\n\nValidation: npm test" }); + + let liveStateFetches90 = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // The LIVE state of the lower sibling #90 is CLOSED, contradicting the stale-cached "open" DB row -- + // reconcileLiveDuplicateSiblings must discover this via a genuine live fetch, not the cache. + if (/\/pulls\/90(?:\?|$)/.test(url)) { + liveStateFetches90 += 1; + return Response.json({ number: 90, state: "closed" }); + } + // Includes a test-file change alongside the code change so missingTestEvidence never confounds the + // duplicateClusterMembership assertion below — this test isolates the ONE slop signal under test. + if (url.includes("/pulls/95/files")) + return Response.json([ + { filename: "src/cache.ts", status: "modified", additions: 12, deletions: 0, changes: 12 }, + { filename: "test/unit/cache.test.ts", status: "modified", additions: 8, deletions: 0, changes: 8 }, + ]); + if (url.includes("/commits/win95/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "PATCH") return Response.json({ id: 970 }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 970 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "dup-winner-slop-drift", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 95, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win95" }, labels: [], body: "Fixes #1\n\nValidation: npm test" }, + }, + }); + + // A genuine live reconciliation happened (proving the fix reads live state, not the stale cache). + expect(liveStateFetches90).toBeGreaterThan(0); + // #95 is correctly credited as the cluster winner: no duplicateClusterMembership slop penalty persisted. + const winnerPr = await env.DB.prepare("select slop_risk, slop_band from pull_requests where repo_full_name = ? and number = ?").bind("JSONbored/gittensory", 95).first<{ slop_risk: number | null; slop_band: string | null }>(); + expect(winnerPr?.slop_band).toBe("clean"); + expect(winnerPr?.slop_risk).toBe(0); + }); + + it("overrides the Gate to neutral for THIS commit only when a real write/admin maintainer runs gate-override", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 90, + title: "Override me", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "override-sha" }, + labels: [], + body: "Validation: npm test", + }); + const calls = { token: 0, permission: 0, checkGets: 0, checkPatches: 0, commentGets: 0, commentPatches: 0 }; + const patchBodies: Array<{ status?: string; conclusion?: string; output?: { title?: string; text?: string } }> = []; + let confirmationBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + // Authorization MUST come from the real collaborator-permission API, never the comment author_association. + if (url.includes("/collaborators/maintainer/permission")) { + calls.permission += 1; + return Response.json({ permission: "admin" }); + } + if (url.includes("/commits/override-sha/check-runs") && method === "GET") { + calls.checkGets += 1; + return Response.json({ total_count: 1, check_runs: [{ id: 555, name: "Gittensory Orb Review Agent" }] }); + } + if (url.includes("/check-runs/555") && method === "PATCH") { + calls.checkPatches += 1; + patchBodies.push(JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string; text?: string } }); + return Response.json({ id: 555 }); + } + if (url.includes("/issues/90/comments") && method === "GET") { + calls.commentGets += 1; + return Response.json([]); + } + if (url.includes("/issues/90/comments") && method === "POST") { + calls.commentPatches += 1; + confirmationBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 9100 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-override-allow", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 90, title: "Override me", state: "open", user: { login: "contributor" }, pull_request: {} }, + // author_association lies (says OWNER); the handler must IGNORE it and use real permission instead. + comment: { id: 800, body: "@gittensory gate-override known flaky duplicate check, shipping", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + // The existing Gate run (id 555) was PATCHed to a neutral, non-blocking terminal state — not a new check. + expect(calls.checkPatches).toBe(1); + const finalize = patchBodies[0]; + expect(finalize?.status).toBe("completed"); + expect(finalize?.conclusion).toBe("neutral"); + expect(finalize?.output?.title).toBe("Gittensory Orb Review Agent — overridden by @maintainer"); + expect(finalize?.output?.text).toContain("Overridden by @maintainer: known flaky duplicate check, shipping"); + expect(confirmationBody).toContain("Gittensory Orb Review Agent overridden by @maintainer"); + const audit = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") + .bind("github_app.gate_overridden") + .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); + expect(audit).toMatchObject({ event_type: "github_app.gate_overridden", actor: "maintainer", target_key: "JSONbored/gittensory#90", outcome: "completed" }); + const usageEvents = await listProductUsageEvents(env, { limit: 10 }); + expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ surface: "github_app", eventName: "gate_overridden", outcome: "completed" })])); + // No override state is persisted: the gate stays "enabled" and the override does NOT persist an advisory, + // so a follow-up synchronize re-evaluates the Gate from scratch (no permanent bypass). + const settingsAfter = await env.DB.prepare("select gate_check_mode from repository_settings where repo_full_name = ?").bind("JSONbored/gittensory").first<{ gate_check_mode: string }>(); + expect(settingsAfter?.gate_check_mode).toBe("enabled"); + const overrideAdvisory = await env.DB.prepare("select id from advisories where target_key = ?").bind("JSONbored/gittensory#90").first<{ id: string }>(); + expect(overrideAdvisory ?? null).toBeNull(); + }); + + it("a real gate-override still completes even when the false-positive telemetry write fails (best-effort)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 94, + title: "Override me (telemetry write fails)", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "override-sha-telemetry" }, + labels: [], + body: "Validation: npm test", + }); + const telemetrySpy = vi.spyOn(repositoriesModule, "markGateOutcomeOverridden").mockRejectedValueOnce(new Error("D1 write failed")); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/commits/override-sha-telemetry/check-runs") && method === "GET") { + return Response.json({ total_count: 1, check_runs: [{ id: 559, name: "Gittensory Orb Review Agent" }] }); + } + if (url.includes("/check-runs/559") && method === "PATCH") return Response.json({ id: 559 }); + if (url.includes("/issues/94/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/94/comments") && method === "POST") return Response.json({ id: 9104 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-override-telemetry-fail", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 94, title: "Override me (telemetry write fails)", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 812, body: "@gittensory gate-override known flaky", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(telemetrySpy).toHaveBeenCalled(); + // The override itself (audit + usage) still completed — the false-positive flag is best-effort and never + // affects the primary override outcome. + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? and target_key = ?") + .bind("github_app.gate_overridden", "JSONbored/gittensory#94") + .first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + }); + + it("gate-override respects agentPaused — never flips the live check-run or posts a confirmation comment (#2256)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + agentPaused: true, + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 91, + title: "Override me while paused", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "paused-override-sha" }, + labels: [], + body: "Validation: npm test", + }); + const calls = { checkPatches: 0, commentPosts: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/commits/paused-override-sha/check-runs") && method === "GET") { + return Response.json({ total_count: 1, check_runs: [{ id: 556, name: "Gittensory Orb Review Agent" }] }); + } + if (url.includes("/check-runs/556") && method === "PATCH") { + calls.checkPatches += 1; + return Response.json({ id: 556 }); + } + if (url.includes("/issues/91/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/91/comments") && method === "POST") { + calls.commentPosts += 1; + return Response.json({ id: 9101 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-override-paused", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 91, title: "Override me while paused", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 810, body: "@gittensory gate-override please", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + // Neither write reached GitHub — a pause must stop this exactly like every other agent-driven write. + expect(calls.checkPatches).toBe(0); + expect(calls.commentPosts).toBe(0); + // REGRESSION: a paused command must not be audited/usage-tracked as a real, completed override. + const overridden = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.gate_overridden").first<{ id: string }>(); + expect(overridden).toBeUndefined(); + const skipped = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.gate_override_skipped").first<{ outcome: string; detail: string }>(); + expect(skipped).toMatchObject({ outcome: "completed", detail: "agent_paused" }); + const usageEvents = await listProductUsageEvents(env, { limit: 10 }); + expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ eventName: "gate_override_skipped", outcome: "skipped" })])); + expect(usageEvents.some((event) => event.eventName === "gate_overridden")).toBe(false); + }); + + it("gate-override respects agentDryRun on a PR with no head sha — records dry_run, not agent_paused (#2256)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + agentDryRun: true, + }); + // No head sha — also exercises the metadata's `?? null` fallback on the skip-path audit/usage records. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 93, + title: "Override me (dry-run, no head)", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: {}, + labels: [], + body: "Validation: npm test", + }); + const calls = { checkPatches: 0, commentPosts: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/pulls/93") && method === "GET") return Response.json({ number: 93, state: "open", head: {} }); + if (url.includes("/issues/93/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/93/comments") && method === "POST") { + calls.commentPosts += 1; + return Response.json({ id: 9103 }); + } + if (url.includes("/check-runs") && method === "PATCH") { + calls.checkPatches += 1; + return Response.json({ id: 557 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-override-dry-run-no-head", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 93, title: "Override me (dry-run, no head)", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 811, body: "@gittensory gate-override please", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(calls.checkPatches).toBe(0); + expect(calls.commentPosts).toBe(0); + const skipped = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.gate_override_skipped") + .first<{ outcome: string; detail: string; metadata_json: string }>(); + expect(skipped).toMatchObject({ outcome: "completed", detail: "dry_run" }); + const metadata = JSON.parse(skipped?.metadata_json ?? "{}") as { headSha?: string | null; cachedHeadSha?: string | null; mode?: string }; + expect(metadata.headSha).toBeNull(); + expect(metadata.cachedHeadSha).toBeNull(); + expect(metadata.mode).toBe("dry_run"); + const usageEvents = await listProductUsageEvents(env, { limit: 10 }); + expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ eventName: "gate_override_skipped", outcome: "skipped" })])); + }); + + it("overrides the LIVE head, not the stale cached SHA, when a commit landed after the command (#16)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + }); + // The stored row still carries the OLD head; a new commit ("live-sha") landed between the comment and now. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 90, + title: "Override me", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "stale-sha" }, + labels: [], + body: "Validation: npm test", + }); + const seen = { staleCheckGets: 0, liveCheckGets: 0, liveLegacyCheckGets: 0 }; + const patchBodies: Array<{ conclusion?: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + // The LIVE head re-fetch — the row says stale-sha but GitHub's head is now live-sha. + if (url.includes("/pulls/90") && method === "GET") return Response.json({ number: 90, state: "open", head: { sha: "live-sha" } }); + if (url.includes("/commits/stale-sha/check-runs") && method === "GET") { + seen.staleCheckGets += 1; + return Response.json({ total_count: 0, check_runs: [] }); + } + if (url.includes("/commits/live-sha/check-runs") && method === "GET") { + const checkName = new URL(url).searchParams.get("check_name"); + if (checkName === "Gittensory Gate") { + seen.liveLegacyCheckGets += 1; + return Response.json({ total_count: 0, check_runs: [] }); + } + seen.liveCheckGets += 1; + return Response.json({ total_count: 1, check_runs: [{ id: 556, name: "Gittensory Orb Review Agent" }] }); + } + if (url.includes("/check-runs/556") && method === "PATCH") { + patchBodies.push(JSON.parse(String(init?.body ?? "{}")) as { conclusion?: string }); + return Response.json({ id: 556 }); + } + if (url.includes("/issues/90/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/90/comments") && method === "POST") return Response.json({ id: 9101 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-override-live-head", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 90, title: "Override me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 803, body: "@gittensory gate-override flaky", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + // The neutral PATCH targeted the LIVE head's Gate run (id 556), and the stale SHA was never touched. + expect(seen.liveCheckGets).toBe(1); + expect(seen.liveLegacyCheckGets).toBe(1); + expect(seen.staleCheckGets).toBe(0); + expect(patchBodies[0]?.conclusion).toBe("neutral"); + const audit = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.gate_overridden") + .first<{ metadata_json: string }>(); + const metadata = JSON.parse(audit?.metadata_json ?? "{}") as { headSha?: string; cachedHeadSha?: string }; + expect(metadata.headSha).toBe("live-sha"); + expect(metadata.cachedHeadSha).toBe("stale-sha"); + }); + + it("records null head SHAs in the override audit when the PR head is unresolved (#16 fail-safe)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + }); + // A cached row with no head SHA (never detail-synced); the live fetch also yields no head. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 90, + title: "Override me", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: {}, + labels: [], + body: "Validation: npm test", + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/pulls/90") && method === "GET") return Response.json({ number: 90, state: "open", head: {} }); + if (url.includes("/issues/90/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/90/comments") && method === "POST") return Response.json({ id: 9102 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-override-null-head", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 90, title: "Override me", state: "open", user: { login: "contributor" }, pull_request: {} }, + // No reason after the command — exercises the "No reason provided." fallback too. + comment: { id: 804, body: "@gittensory gate-override", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const audit = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") + .bind("github_app.gate_overridden") + .first<{ detail: string; metadata_json: string }>(); + expect(audit?.detail).toBe("No reason provided."); + const metadata = JSON.parse(audit?.metadata_json ?? "{}") as { headSha?: string | null; cachedHeadSha?: string | null }; + expect(metadata.headSha).toBeNull(); + expect(metadata.cachedHeadSha).toBeNull(); + }); + + it("ignores gate-override commands on edited comments", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 92, + title: "Edited override", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "edited-override" }, + labels: [], + body: "Validation: npm test", + }); + const calls = { token: 0, permission: 0, checkRuns: 0, comments: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + if (url.includes("/collaborators/")) { + calls.permission += 1; + return Response.json({ permission: "admin" }); + } + if (url.includes("/check-runs")) { + calls.checkRuns += 1; + return Response.json({ total_count: 1, check_runs: [{ id: 556, name: "Gittensory Orb Review Agent" }] }); + } + if (url.includes("/comments")) { + calls.comments += 1; + return Response.json([]); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-override-edited", + eventName: "issue_comment", + payload: { + action: "edited", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 92, title: "Edited override", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 802, body: "@gittensory gate-override edited by moderator", author_association: "OWNER", user: { login: "maintainer", type: "User" } }, + sender: { login: "moderator", type: "User" }, + }, + }); + + expect(calls.permission).toBe(0); + expect(calls.checkRuns).toBe(0); + expect(calls.comments).toBe(0); + const overridden = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.gate_overridden").first<{ id: string }>(); + expect(overridden ?? null).toBeNull(); + const skipped = await env.DB.prepare("select actor, detail from audit_events where event_type = ?").bind("github_app.gate_override_skipped").first<{ actor: string; detail: string }>(); + expect(skipped).toMatchObject({ actor: "moderator", detail: "unsupported_comment_action" }); + }); + + it("denies gate-override from an org member without real repository write/admin (ignores author_association)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 91, + title: "Cannot override", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: "override-denied" }, + labels: [], + body: "Validation: npm test", + }); + const calls = { token: 0, permission: 0, checkGets: 0, checkPatches: 0, comments: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) { + calls.token += 1; + return Response.json({ token: "installation-token" }); + } + // Real permission is only "read" — even though the comment claims MEMBER, the Gate must NOT be touched. + if (url.includes("/collaborators/org-member/permission")) { + calls.permission += 1; + return Response.json({ permission: "read" }); + } + if (url.includes("/check-runs")) { + calls.checkGets += 1; + return new Response("not found", { status: 404 }); + } + if (url.includes("/comments")) { + calls.comments += 1; + return Response.json([]); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-override-deny", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 91, title: "Cannot override", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 801, body: "@gittensory gate-override trust me", author_association: "MEMBER", user: { login: "org-member", type: "User" } }, + sender: { login: "org-member", type: "User" }, + }, + }); + + // Authorization denied via real permission: no Gate check call and no comment were made. + expect(calls.permission).toBe(1); + expect(calls.checkGets).toBe(0); + expect(calls.checkPatches).toBe(0); + expect(calls.comments).toBe(0); + const denied = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") + .bind("github_app.gate_override_denied") + .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); + expect(denied).toMatchObject({ event_type: "github_app.gate_override_denied", actor: "org-member", target_key: "JSONbored/gittensory#91", outcome: "denied", detail: "not_maintainer_or_pr_author" }); + const overridden = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.gate_overridden").first<{ id: string }>(); + expect(overridden ?? null).toBeNull(); + }); + + // #1964 (record slice): `@gittensory resolve` records review-memory suppression signals for advisory warnings. + describe("@gittensory resolve (#1964)", () => { + async function seedResolvePr(env: Env, repoFullName: string, prNumber: number, headSha: string) { + const slash = repoFullName.indexOf("/"); + const owner = slash >= 0 ? repoFullName.slice(0, slash) : repoFullName; + const name = slash >= 0 ? repoFullName.slice(slash + 1) : repoFullName; + await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); + await upsertRepositorySettings(env, { + repoFullName, + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + requireLinkedIssue: true, + linkedIssueGateMode: "advisory", + aiReviewMode: "advisory", + }); + await upsertPullRequestFromGitHub(env, repoFullName, { + number: prNumber, + title: "Resolve me", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: headSha }, + labels: [], + body: "No linked issue on purpose", + }); + } + + it("records a suppression signal and finding_resolved when an authorized maintainer resolves a named warning with review.memory ON", async () => { + const repoFullName = "JSONbored/resolve-1964-a"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_MEMORY: "true", + }); + await seedResolvePr(env, repoFullName, 1964, "resolve-1964-a"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + const calls = { permission: 0, checkPatches: 0, comments: 0 }; + let confirmationBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) { + calls.permission += 1; + return Response.json({ permission: "admin" }); + } + if (url.includes("/issues/1964/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/1964/comments") && method === "POST") { + calls.comments += 1; + confirmationBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 19641 }); + } + if (url.includes("/check-runs") && method === "PATCH") { + calls.checkPatches += 1; + return Response.json({ id: 1 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1964-allow", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1964-a", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1964, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { + id: 19640, + body: "@gittensory resolve missing_linked_issue", + author_association: "NONE", + user: { login: "maintainer", type: "User" }, + }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(calls.permission).toBe(1); + expect(calls.checkPatches).toBe(0); + expect(calls.comments).toBe(1); + expect(confirmationBody).toContain("Review finding resolved"); + expect(confirmationBody).toContain("missing_linked_issue"); + expect(confirmationBody).toContain("Gate check-run is unchanged"); + const resolved = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?") + .bind("github_app.finding_resolved") + .first<{ outcome: string; detail: string }>(); + expect(resolved).toMatchObject({ outcome: "completed" }); + const memoryRecorded = await env.DB.prepare("select outcome from audit_events where event_type = ?") + .bind("github_app.review_memory_recorded") + .first<{ outcome: string }>(); + expect(memoryRecorded).toMatchObject({ outcome: "completed" }); + const suppressions = await listReviewSuppressions(env, repoFullName); + expect(suppressions).toHaveLength(1); + expect(suppressions[0]).toMatchObject({ + category: "missing_linked_issue", + createdBy: "maintainer", + }); + }); + + it("records a suppression for a current cached AI review warning", async () => { + const repoFullName = "JSONbored/resolve-1964-ai-cached"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1974, "resolve-1964-ai-cached"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + await putCachedAiReview(env, repoFullName, 1974, "resolve-1964-ai-cached", "advisory", { + notes: "The cached AI review found a public issue.", + reviewerCount: 2, + findings: [{ code: "ai_review_split", severity: "warning", title: "AI reviewers disagree", detail: "One reviewer flagged a likely defect that needs maintainer triage." }], + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/1974/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/1974/comments") && method === "POST") return Response.json({ id: 19741 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1974-ai-cached", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1964-ai-cached", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1974, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19740, body: "@gittensory resolve ai_review_split", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const suppressions = await listReviewSuppressions(env, repoFullName); + expect(suppressions).toHaveLength(1); + expect(suppressions[0]).toMatchObject({ category: "ai_review_split", createdBy: "maintainer" }); + const resolved = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.finding_resolved") + .first<{ metadata_json: string }>(); + expect(JSON.parse(resolved?.metadata_json ?? "{}")).toMatchObject({ findingCode: "ai_review_split", resolvedWarningCount: 1, recordedSuppressionCount: 1 }); + }); + + it("falls back to the last published public AI review when the current cached review has no public assessment", async () => { + const repoFullName = "JSONbored/resolve-1964-ai-published"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1975, "resolve-1964-ai-current"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + await putCachedAiReview(env, repoFullName, 1975, "resolve-1964-ai-old", "advisory", { + notes: "The published AI review found a public consensus defect.", + reviewerCount: 2, + findings: [{ code: "ai_consensus_defect", severity: "warning", title: "AI reviewers agree on a defect", detail: "Both reviewers flagged the same likely defect for maintainer triage." }], + }); + await markAiReviewPublished(env, repoFullName, 1975, "resolve-1964-ai-old"); + await putCachedAiReview(env, repoFullName, 1975, "resolve-1964-ai-current", "advisory", { + notes: "", + reviewerCount: 2, + findings: [{ code: "ai_review_split", severity: "warning", title: "Hidden", detail: "No public assessment." }], + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/1975/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/1975/comments") && method === "POST") return Response.json({ id: 19751 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1975-ai-published", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1964-ai-published", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1975, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19750, body: "@gittensory resolve ai_consensus_defect", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const suppressions = await listReviewSuppressions(env, repoFullName); + expect(suppressions).toHaveLength(1); + expect(suppressions[0]?.category).toBe("ai_consensus_defect"); + }); + + it("records finding_resolved without a suppression write when review.memory is OFF (operator kill-switch)", async () => { + const repoFullName = "JSONbored/resolve-1965-off"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedResolvePr(env, repoFullName, 1965, "resolve-1965-off"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/1965/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/1965/comments") && method === "POST") return Response.json({ id: 19651 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1965-flag-off", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1965-off", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1965, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { + id: 19650, + body: "@gittensory resolve missing_linked_issue", + author_association: "NONE", + user: { login: "maintainer", type: "User" }, + }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const memoryRecorded = await env.DB.prepare("select id from audit_events where event_type = ?") + .bind("github_app.review_memory_recorded") + .first<{ id: string }>(); + expect(memoryRecorded ?? null).toBeNull(); + expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); + const resolved = await env.DB.prepare("select outcome from audit_events where event_type = ?") + .bind("github_app.finding_resolved") + .first<{ outcome: string }>(); + expect(resolved).toMatchObject({ outcome: "completed" }); + }); + + it("denies an unauthorized actor and records no suppression signal", async () => { + const repoFullName = "JSONbored/resolve-1966-deny"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1966, "resolve-1966-deny"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/org-member/permission")) return Response.json({ permission: "read" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1966-deny", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1966-deny", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1966, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { + id: 19660, + body: "@gittensory resolve missing_linked_issue", + author_association: "MEMBER", + user: { login: "org-member", type: "User" }, + }, + sender: { login: "org-member", type: "User" }, + }, + }); + + const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?") + .bind("github_app.finding_resolved_denied") + .first<{ outcome: string }>(); + expect(denied).toMatchObject({ outcome: "denied" }); + expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); + }); + + it.each([ + ["malformed finding id", "@gittensory resolve ../escape", "malformed_finding_id"], + ["absent finding code", "@gittensory resolve readiness_score_below_threshold", "finding_not_found"], + ] as const)("skips resolve when the maintainer supplies %s", async (_label, body, reason) => { + const repoFullName = "JSONbored/resolve-1967-skip"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1967, "resolve-1967-skip"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: `resolve-1967-${reason}`, + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1967-skip", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1967, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19670, body, author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.finding_resolved_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe(reason); + expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); + }); + + it("records every current advisory warning for a whole-PR `@gittensory resolve` ack", async () => { + const repoFullName = "JSONbored/resolve-1968-whole"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1968, "resolve-1968-whole"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/1968/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/1968/comments") && method === "POST") return Response.json({ id: 19681 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1968-whole", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1968-whole", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1968, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19680, body: "@gittensory resolve", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(2); + const resolved = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.finding_resolved") + .first<{ metadata_json: string }>(); + expect(JSON.parse(resolved?.metadata_json ?? "{}")).toMatchObject({ scope: "whole_pr", resolvedWarningCount: 2 }); + }); + + it("ignores issue comments that are not @gittensory resolve commands (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1973-plain"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedResolvePr(env, repoFullName, 1973, "resolve-1973-plain"); + let commentPosts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/1973/comments") && method === "POST") { + commentPosts += 1; + return Response.json({ id: 19730 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-plain-comment", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1973-plain", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1973, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19731, body: "Looks good to me", author_association: "OWNER", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + expect(commentPosts).toBe(0); + const events = await env.DB.prepare("select event_type from audit_events where event_type like ?").bind("github_app.finding_resolved%").all<{ event_type: string }>(); + expect(events.results ?? []).toEqual([]); + }); + + it("ignores other @gittensory verbs on the resolve handler path (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1974-help"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedResolvePr(env, repoFullName, 1974, "resolve-1974-help"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-help-verb", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1974-help", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1974, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19740, body: "@gittensory help", author_association: "OWNER", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const events = await env.DB.prepare("select event_type from audit_events where event_type like ?").bind("github_app.finding_resolved%").all<{ event_type: string }>(); + expect(events.results ?? []).toEqual([]); + }); + + it("skips resolve when the webhook payload lacks a repository (#1964)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-missing-repo", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + issue: { number: 1972, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19720, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("missing_repo_pr_installation_or_actor"); + }); + + it("skips resolve when the cached pull request row is missing (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1969-missing-pr"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1969-missing-pr", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1969-missing-pr", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1969, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19690, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("cached_pr_missing"); + }); + + it("skips resolve in agentDryRun without recording finding_resolved (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1970-dry-run"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1970, "resolve-1970-dry-run"); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: true, linkedIssueGateMode: "advisory", agentDryRun: true }); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1970-dry-run", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1970-dry-run", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1970, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19700, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("dry_run"); + const resolved = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.finding_resolved").first<{ id: string }>(); + expect(resolved ?? null).toBeNull(); + }); + + it("skips resolve when the repository is agentPaused (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1971-paused"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1971, "resolve-1971-paused"); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: true, linkedIssueGateMode: "advisory", agentPaused: true }); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1971-paused", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1971-paused", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1971, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19710, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("agent_paused"); + }); + }); + + // #2169 (part of #1960): `@gittensory explain ` echoes an already-generated finding's public-safe + // rationale on the PR thread — read-only, no model call, no mutation. Mirrors the `resolve` harness above. + describe("@gittensory explain (#2169)", () => { + async function seedExplainPr(env: Env, repoFullName: string, prNumber: number, headSha: string) { + const slash = repoFullName.indexOf("/"); + const owner = repoFullName.slice(0, slash); + const name = repoFullName.slice(slash + 1); + await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: true, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); + await upsertPullRequestFromGitHub(env, repoFullName, { number: prNumber, title: "Explain me", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: headSha }, labels: [], body: "No linked issue on purpose" }); + } + const explainWebhook = (repoFullName: string, prNumber: number, body: string, actor: string, opts: { association?: string; bot?: boolean; action?: string } = {}) => ({ + type: "github-webhook" as const, + deliveryId: `explain-${prNumber}-${actor}`, + eventName: "issue_comment" as const, + payload: { + action: opts.action ?? "created", + installation: { id: 123, account: { login: repoFullName.slice(0, repoFullName.indexOf("/")), id: 1, type: "User" } }, + repository: { name: repoFullName.slice(repoFullName.indexOf("/") + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, repoFullName.indexOf("/")) } }, + issue: { number: prNumber, title: "Explain me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: prNumber * 10, body, author_association: opts.association ?? "NONE", user: { login: actor, type: opts.bot ? "Bot" : "User" } }, + sender: { login: actor, type: opts.bot ? "Bot" : "User" }, + }, + }) as unknown as Parameters[1]; + + it("echoes a named finding's stored rationale to an authorized maintainer + records finding_explained (no mutation)", async () => { + const repoFullName = "JSONbored/explain-2169-echo"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedExplainPr(env, repoFullName, 2169, "explain-2169-echo"); + await putCachedAiReview(env, repoFullName, 2169, "explain-2169-echo", "advisory", { + notes: "The cached AI review found a public issue.", + reviewerCount: 2, + findings: [{ code: "ai_review_split", severity: "warning", title: "AI reviewers disagree", detail: "One reviewer flagged a likely defect that needs maintainer triage." }], + }); + let postedBody = ""; + const calls = { comments: 0, checkPatches: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/2169/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/2169/comments") && method === "POST") { calls.comments += 1; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 21690 }); } + if (url.includes("/check-runs") && method === "PATCH") { calls.checkPatches += 1; return Response.json({ id: 1 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, explainWebhook(repoFullName, 2169, "@gittensory explain ai_review_split", "maintainer")); + + expect(calls.comments).toBe(1); + expect(calls.checkPatches).toBe(0); // read-only: never touches the gate check-run + expect(postedBody).toContain("Explanation of `ai_review_split`"); + expect(postedBody).toContain("AI reviewers disagree"); // the finding's stored title + expect(postedBody).toContain("One reviewer flagged a likely defect"); // its stored rationale, echoed verbatim + const explained = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("github_app.finding_explained").first<{ outcome: string; metadata_json: string }>(); + expect(explained?.outcome).toBe("completed"); + expect(JSON.parse(explained?.metadata_json ?? "{}")).toMatchObject({ findingCode: "ai_review_split", explainedCount: 1 }); + }); + + it("echoes a deterministic finding's rationale AND its suggested action", async () => { + const repoFullName = "JSONbored/explain-2169-action"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + // seedExplainPr sets requireLinkedIssue + a body with no linked issue, so the gate yields the deterministic + // `missing_linked_issue` warning, which carries a `detail` AND an `action` (src/rules/advisory.ts). + await seedExplainPr(env, repoFullName, 2176, "explain-2169-action"); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/2176/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/2176/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 21760 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, explainWebhook(repoFullName, 2176, "@gittensory explain missing_linked_issue", "maintainer")); + + expect(postedBody).toContain("No linked issue detected"); // title + expect(postedBody).toContain("Suggested action:"); // the finding's action is rendered + expect(postedBody).toContain("link it explicitly in the PR body"); // the action text, echoed + const explained = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.finding_explained").first<{ outcome: string }>(); + expect(explained?.outcome).toBe("completed"); + }); + + it("posts a public-safe not-found note when the finding id is unknown", async () => { + const repoFullName = "JSONbored/explain-2169-missing"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedExplainPr(env, repoFullName, 2170, "explain-2169-missing"); + let postedBody = ""; + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/2170/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/2170/comments") && method === "POST") { posted = true; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 21700 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, explainWebhook(repoFullName, 2170, "@gittensory explain readiness_score_below_threshold", "maintainer")); + + expect(posted).toBe(true); + expect(postedBody).toContain("No review finding `readiness_score_below_threshold`"); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_explained_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("finding_not_found"); + }); + + it.each([ + ["missing argument", "@gittensory explain", "missing_finding_argument"], + ["malformed finding id", "@gittensory explain ../escape", "malformed_finding_id"], + ] as const)("skips (no comment) when the maintainer supplies %s", async (_label, body, reason) => { + const repoFullName = "JSONbored/explain-2169-skip"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedExplainPr(env, repoFullName, 2171, "explain-2169-skip"); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/comments") && method === "POST") { posted = true; return Response.json({ id: 1 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, explainWebhook(repoFullName, 2171, body, "maintainer")); + + expect(posted).toBe(false); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_explained_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe(reason); + }); + + it("denies a non-maintainer — no explanation posted, records finding_explained_denied", async () => { + const repoFullName = "JSONbored/explain-2169-deny"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedExplainPr(env, repoFullName, 2172, "explain-2169-deny"); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/org-member/permission")) return Response.json({ permission: "read" }); + if (url.includes("/comments")) { posted = true; return Response.json({ id: 1 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, explainWebhook(repoFullName, 2172, "@gittensory explain ai_review_split", "org-member", { association: "MEMBER" })); + + expect(posted).toBe(false); + const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.finding_explained_denied").first<{ outcome: string }>(); + expect(denied).toMatchObject({ outcome: "denied" }); + }); + + it("records a classifier skip for a bot-authored explain command, never acting on it", async () => { + const repoFullName = "JSONbored/explain-2169-bot"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedExplainPr(env, repoFullName, 2173, "explain-2169-bot"); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + + await processJob(env, explainWebhook(repoFullName, 2173, "@gittensory explain ai_review_split", "some-bot[bot]", { bot: true })); + + expect(posted).toBe(false); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_explained_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("bot_author"); + }); + + it("skips with cached_pr_missing when the referenced PR is not in the local store", async () => { + const repoFullName = "JSONbored/explain-2169-nopr"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + // Register the repo + settings but NOT the PR row, so getPullRequest returns null. + await upsertRepositoryFromGitHub(env, { name: "explain-2169-nopr", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName, gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "advisory" }); + let posted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/comments")) posted = true; + return new Response("not found", { status: 404 }); + }); + + await processJob(env, explainWebhook(repoFullName, 2174, "@gittensory explain ai_review_split", "maintainer")); + + expect(posted).toBe(false); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_explained_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("cached_pr_missing"); + }); + + it("declines (returns false) for a non-explain comment and for a plain non-mention comment", async () => { + const repoFullName = "JSONbored/explain-2169-decline"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedExplainPr(env, repoFullName, 2175, "explain-2169-decline"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/2175/comments") && !url.includes("POST")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, explainWebhook(repoFullName, 2175, "just a normal comment, no mention", "maintainer")); + await processJob(env, explainWebhook(repoFullName, 2175, "@gittensory configuration", "maintainer")); + + // The explain handler never claimed either comment — no explain audit rows at all. + const explainRows = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.finding_explained%'").first<{ n: number }>(); + expect(explainRows?.n).toBe(0); + }); + }); + + // #4195 (part of the #4189 E2E-test-generation epic): `@gittensory generate-tests` -- on-demand, + // MAINTAINER-ONLY AI-generated E2E test coverage, posted as its own reply comment. Mirrors the explain + // harness above (classify -> authorize -> act -> audit), but with the authorization tier deliberately + // narrowed to ["maintainer"] only -- no collaborator, no confirmed_miner -- and a real (mocked) model call. + describe("@gittensory generate-tests (#4195)", () => { + async function seedGenerateTestsPr( + env: Env, + repoFullName: string, + prNumber: number, + headSha: string, + authorLogin = "contributor", + opts: { headRef?: string; e2eTestDelivery?: "comment" | "commit" } = {}, + ) { + const slash = repoFullName.indexOf("/"); + const owner = repoFullName.slice(0, slash); + const name = repoFullName.slice(slash + 1); + await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); + await upsertPullRequestFromGitHub(env, repoFullName, { number: prNumber, title: "Add retry to checkout", state: "open", user: { login: authorLogin }, author_association: "CONTRIBUTOR", head: { sha: headSha, ref: opts.headRef ?? "feature/checkout-retry" }, labels: [], body: "Retries the payment call once on a 5xx." }); + await upsertPullRequestFile(env, { repoFullName, pullNumber: prNumber, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); + // A renamed-with-no-patch file (GitHub omits `patch` for pure renames) -- exercises the + // payload?.patch-is-not-a-string branch in the files.map() that builds E2eTestGenChangedFile[]. + await upsertPullRequestFile(env, { repoFullName, pullNumber: prNumber, path: "src/renamed.ts", status: "renamed", additions: 0, deletions: 0, changes: 0, payload: {} }); + // features.e2eTests + review.e2e_test_delivery MUST land in the SAME upsertRepoFocusManifest call -- + // a second separate call REPLACES rather than merges with a prior one (see repo-doc-pr.test.ts). + await upsertRepoFocusManifest(env, repoFullName, { + features: { e2eTests: true }, + ...(opts.e2eTestDelivery ? { review: { e2e_test_delivery: opts.e2eTestDelivery } } : {}), + }); + } + const generateTestsWebhook = (repoFullName: string, prNumber: number, actor: string, opts: { association?: string; bot?: boolean; commenterIsAuthor?: boolean } = {}) => ({ + type: "github-webhook" as const, + deliveryId: `generate-tests-${prNumber}-${actor}`, + eventName: "issue_comment" as const, + payload: { + action: "created", + installation: { id: 123, account: { login: repoFullName.slice(0, repoFullName.indexOf("/")), id: 1, type: "User" } }, + repository: { name: repoFullName.slice(repoFullName.indexOf("/") + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, repoFullName.indexOf("/")) } }, + issue: { number: prNumber, title: "Add retry to checkout", state: "open", user: { login: opts.commenterIsAuthor ? actor : "contributor" }, pull_request: {} }, + comment: { id: prNumber * 10, body: "@gittensory generate-tests", author_association: opts.association ?? "NONE", user: { login: actor, type: opts.bot ? "Bot" : "User" } }, + sender: { login: actor, type: opts.bot ? "Bot" : "User" }, + }, + }) as unknown as Parameters[1]; + const VALID_TEST_SOURCE = "import { test, expect } from '@playwright/test';\n\ntest('checkout retries on failure', async ({ page }) => {\n await page.goto('/checkout');\n await expect(page.getByRole('button', { name: 'Pay' })).toBeVisible();\n});"; + + it("generates and posts an E2E test for an authorized maintainer, and records a completed audit event", async () => { + const repoFullName = "JSONbored/gen-tests-4195-ok"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedGenerateTestsPr(env, repoFullName, 4195, "gen-tests-4195-ok"); + let postedBody = ""; + let posted = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/4195/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4195/comments") && method === "POST") { posted += 1; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 41950 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4195, "maintainer", { association: "MEMBER" })); + + expect(posted).toBe(1); + expect(postedBody).toContain("AI-generated Playwright test for @maintainer"); + expect(postedBody).toContain("test('checkout retries on failure'"); + const audited = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ outcome: string; metadata_json: string }>(); + expect(audited?.outcome).toBe("completed"); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ status: "ok", byok: false }); + }); + + it("denies a collaborator-tier actor (write permission, not the PR author) — narrower than every other command", async () => { + const repoFullName = "JSONbored/gen-tests-4195-collab"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedGenerateTestsPr(env, repoFullName, 4196, "gen-tests-4195-collab"); + let posted = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/writer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/4196/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4196/comments") && method === "POST") { posted += 1; return Response.json({ id: 41960 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4196, "writer", { association: "COLLABORATOR" })); + + expect(posted).toBe(0); // denied before any generation or comment + const denied = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_denied").first<{ outcome: string; detail: string }>(); + expect(denied?.outcome).toBe("denied"); + }); + + it("denies the PR's own author even though they authored it — the exact loophole a click-to-generate button must not open", async () => { + const repoFullName = "JSONbored/gen-tests-4195-author"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedGenerateTestsPr(env, repoFullName, 4197, "gen-tests-4195-author", "contributor"); + let posted = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // No collaborator/permission relationship at all -- a plain contributor commenting on their own PR. + if (url.includes("/collaborators/contributor/permission")) return new Response("not found", { status: 404 }); + if (url.includes("/issues/4197/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4197/comments") && method === "POST") { posted += 1; return Response.json({ id: 41970 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4197, "contributor", { association: "NONE", commenterIsAuthor: true })); + + expect(posted).toBe(0); + const denied = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_denied").first<{ detail: string }>(); + expect(denied?.detail).toBe("maintainer_command_requires_maintainer"); + }); + + it("falls back to a safe withheld-content note when posting the real generated-test comment fails", async () => { + const repoFullName = "JSONbored/gen-tests-4195-post-fails"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedGenerateTestsPr(env, repoFullName, 4200, "gen-tests-4195-post-fails"); + let postAttempts = 0; + let fallbackBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/4200/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4200/comments") && method === "POST") { + postAttempts += 1; + // The FIRST attempt (the real generated-test comment) fails with a genuine GitHub API error; the + // SECOND attempt (the withheld-content fallback) must still succeed. + if (postAttempts === 1) return new Response("server exploded", { status: 500 }); + fallbackBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 42000 }); + } + return new Response("not found", { status: 404 }); + }); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + await processJob(env, generateTestsWebhook(repoFullName, 4200, "maintainer", { association: "MEMBER" })); + + expect(postAttempts).toBe(2); + expect(fallbackBody).toContain("did not produce a usable result"); + expect(fallbackBody).not.toContain("test('checkout retries on failure'"); + expect(logSpy.mock.calls.map((c) => String(c[0])).some((line) => line.includes("e2e_test_gen_comment_withheld"))).toBe(true); + logSpy.mockRestore(); + }); + + it("posts a not-enabled note (no generation call) when features.e2eTests is off for the repo", async () => { + const repoFullName = "JSONbored/gen-tests-4195-disabled"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); + await upsertPullRequestFromGitHub(env, repoFullName, { number: 4198, title: "x", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "gen-tests-4195-disabled" }, labels: [], body: "x" }); + // Deliberately no upsertRepoFocusManifest features.e2eTests override -- stays off (no allowlist either). + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/4198/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4198/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 41980 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4198, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("E2E test generation is not enabled for this repository"); + expect(run).not.toHaveBeenCalled(); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("feature_disabled"); + }); + + it("posts a did-not-produce-a-usable-result note when the model output never parses", async () => { + const repoFullName = "JSONbored/gen-tests-4195-garbage"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "not a test file" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedGenerateTestsPr(env, repoFullName, 4199, "gen-tests-4195-garbage"); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/4199/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4199/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 41990 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4199, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("did not produce a usable result"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ status: "ok" }); + }); + + it("skips cleanly when the cached PR record is missing", async () => { + const repoFullName = "JSONbored/gen-tests-4195-nopr"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); + // No upsertPullRequestFromGitHub -- the PR was never cached. + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4200, "maintainer", { association: "MEMBER" })); + + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("cached_pr_missing"); + }); + + it("declines (returns false) for a non-command comment, claiming nothing", async () => { + const repoFullName = "JSONbored/gen-tests-4195-decline"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedGenerateTestsPr(env, repoFullName, 4201, "gen-tests-4195-decline"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/4201/comments")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + const webhook = generateTestsWebhook(repoFullName, 4201, "maintainer", { association: "MEMBER" }); + (webhook as unknown as { payload: { comment: { body: string } } }).payload.comment.body = "just chatting, no mention here"; + + await processJob(env, webhook); + + const rows = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); + expect(rows?.n).toBe(0); + }); + + it("skips cleanly when the comment classifies as invalid (a bot posted the mention)", async () => { + const repoFullName = "JSONbored/gen-tests-4195-bot"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedGenerateTestsPr(env, repoFullName, 4202, "gen-tests-4195-bot"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4202, "some-bot[bot]", { association: "NONE", bot: true })); + + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("bot_author"); + }); + + it("uses the maintainer's BYOK frontier model (not Workers AI) when aiReviewByok is on and a key is configured", async () => { + const repoFullName = "JSONbored/gen-tests-4195-byok"; + const run = vi.fn(); // Workers AI must NOT be used when BYOK is configured + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + TOKEN_ENCRYPTION_SECRET: "gen-tests-byok-test-encryption-secret-32b", + }); + await seedGenerateTestsPr(env, repoFullName, 4203, "gen-tests-4195-byok"); + // aiReviewProvider set AND matching the stored key's provider -- exercises the "explicit provider + // pin agrees with the stored key" arm, distinct from the (also-tested-elsewhere) "no pin configured" + // default arm. + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory", aiReviewByok: true, aiReviewProvider: "anthropic" }); + await upsertRepositoryAiKey(env, { repoFullName, provider: "anthropic", key: "sk-ant-byok-gen-tests-9999", model: null }); + let postedBody = ""; + const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("api.anthropic.com")) return Response.json({ content: [{ type: "text", text: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }] }); + if (url.includes("/issues/4203/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4203/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42030 }); } + return new Response("not found", { status: 404 }); + }); + vi.stubGlobal("fetch", fetchMock); + + await processJob(env, generateTestsWebhook(repoFullName, 4203, "maintainer", { association: "MEMBER" })); + + expect(run).not.toHaveBeenCalled(); + expect(postedBody).toContain("test('checkout retries on failure'"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ byok: true }); + }); + + it("degrades to the not-usable-result note when the feature is on but no AI provider is configured at all", async () => { + const repoFullName = "JSONbored/gen-tests-4195-unavailable"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedGenerateTestsPr(env, repoFullName, 4204, "gen-tests-4195-unavailable"); // no env.AI, no BYOK key + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/4204/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4204/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42040 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4204, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("did not produce a usable result"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ status: "unavailable" }); + }); + + it("generates via the GITTENSORY_REVIEW_REPOS allowlist default when no manifest is published at all", async () => { + // No upsertRepoFocusManifest call -- loadRepoFocusManifest resolves null, so manifest?.review (fed to + // resolveE2eTestGenInstructions) and the e2eTests feature gate itself both take their null/allowlist path. + const repoFullName = "JSONbored/gen-tests-4195-allowlist"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + GITTENSORY_REVIEW_REPOS: repoFullName, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); + await upsertPullRequestFromGitHub(env, repoFullName, { number: 4205, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "gen-tests-4195-allowlist" }, labels: [], body: "x" }); + await upsertPullRequestFile(env, { repoFullName, pullNumber: 4205, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/4205/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4205/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42050 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4205, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("test('checkout retries on failure'"); + }); + + it("skips cleanly when the webhook payload has no comment object at all", async () => { + const repoFullName = "JSONbored/gen-tests-4195-nocomment"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedGenerateTestsPr(env, repoFullName, 4206, "gen-tests-4195-nocomment"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + const webhook = generateTestsWebhook(repoFullName, 4206, "maintainer", { association: "MEMBER" }); + delete (webhook as unknown as { payload: { comment?: unknown } }).payload.comment; + + await processJob(env, webhook); + + const rows = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); + expect(rows?.n).toBe(0); + }); + + // #4197 (commit delivery) + #4201 (scoring-integrity safeguard), both part of the #4189 epic. + describe("commit delivery mode (#4197, #4201)", () => { + it("pushes the generated test as a commit onto the PR's own head branch for a non-miner author, and records commitStatus: committed", async () => { + const repoFullName = "JSONbored/gen-tests-4197-commit-ok"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedGenerateTestsPr(env, repoFullName, 4207, "commit-ok-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.endsWith("/pulls/4207") && method === "GET") return Response.json({ head: { ref: "feature/checkout-retry", sha: "commit-ok-head-sha", repo: { full_name: repoFullName } } }); + if (url.endsWith("/git/commits/commit-ok-head-sha") && method === "GET") return Response.json({ tree: { sha: "base-tree-sha" } }); + if (url.endsWith("/git/trees") && method === "POST") return Response.json({ sha: "new-tree-sha" }); + if (url.endsWith("/git/commits") && method === "POST") return Response.json({ sha: "committed-sha-123" }); + if (method === "PATCH") return Response.json({}); + if (url.includes("/issues/4207/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4207/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42070 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4207, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("pushed as a commit"); + expect(postedBody).toContain(`https://github.com/${repoFullName}/commit/committed-sha-123`); + expect(postedBody).not.toContain("```typescript"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "committed" }); + }); + + it("blocks commit delivery for a confirmed Gittensor miner PR author, but still posts the generated test as a suggestion (#4201)", async () => { + const repoFullName = "JSONbored/gen-tests-4201-miner-blocked"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedGenerateTestsPr(env, repoFullName, 4208, "miner-blocked-head-sha", "confirmed-miner", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); + await upsertOfficialMinerDetection(env, "confirmed-miner", { status: "confirmed", snapshot: queueMinerSnapshot("confirmed-miner") }, 60_000); + let posted = 0; + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + // No git/trees or git/commits stubs at all -- a blocked commit must never even attempt a GitHub write. + if (url.includes("/issues/4208/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4208/comments") && method === "POST") { posted += 1; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42080 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4208, "maintainer", { association: "MEMBER" })); + + expect(posted).toBe(1); + expect(postedBody).toContain("confirmed Gittensor miner"); + expect(postedBody).toContain("```typescript\n" + VALID_TEST_SOURCE + "\n```"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "blocked" }); + }); + + it("falls back to a declined-with-reason suggestion when commit delivery has no write access to a fork PR branch", async () => { + const repoFullName = "JSONbored/gen-tests-4197-commit-declined"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedGenerateTestsPr(env, repoFullName, 4209, "declined-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.endsWith("/pulls/4209") && method === "GET") return new Response("forbidden", { status: 403 }); + if (url.includes("/issues/4209/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4209/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42090 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4209, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("Commit delivery was requested but declined: no write access"); + expect(postedBody).toContain("```typescript\n" + VALID_TEST_SOURCE + "\n```"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "declined" }); + }); + + it("declines commit delivery with a clear reason when the PR's head branch/commit is not cached at all", async () => { + const repoFullName = "JSONbored/gen-tests-4197-commit-no-head"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); + // No head sha/ref cached at all on this PR record. + await upsertPullRequestFromGitHub(env, repoFullName, { number: 4210, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", labels: [], body: "x" }); + await upsertPullRequestFile(env, { repoFullName, pullNumber: 4210, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); + await upsertRepoFocusManifest(env, repoFullName, { features: { e2eTests: true }, review: { e2e_test_delivery: "commit" } }); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/4210/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4210/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42100 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4210, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("Commit delivery was requested but declined: the PR's head branch/commit is not cached"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "declined" }); + }); + + it("maps a genuinely unexpected git-write failure to a declined outcome (not a thrown error) in the posted comment", async () => { + const repoFullName = "JSONbored/gen-tests-4197-commit-error-mapped"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedGenerateTestsPr(env, repoFullName, 4213, "error-mapped-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + // Neither a 403/404 (no write access) nor a 422/409 (branch moved) -- a genuinely unexpected 500, + // which commitE2eTestToPrBranch maps to status: "error" rather than "declined". + if (url.endsWith("/pulls/4213") && method === "GET") return new Response("server exploded", { status: 500 }); + if (url.includes("/issues/4213/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4213/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42130 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4213, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("Commit delivery was requested but declined:"); + expect(postedBody).toContain("```typescript\n" + VALID_TEST_SOURCE + "\n```"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "declined" }); + }); + + it("still resolves the miner-safeguard check (to not-found) when the cached PR record has no author login at all", async () => { + const repoFullName = "JSONbored/gen-tests-4201-no-author"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); + // Deliberately no `user` field at all -- the cached PR's authorLogin resolves to null, exercising the + // ternary's not-found arm (`pr.authorLogin ? ... : { status: "not_found" }`) instead of ever calling + // getCachedOfficialMinerDetection. + await upsertPullRequestFromGitHub(env, repoFullName, { number: 4214, title: "Add retry to checkout", state: "open", author_association: "CONTRIBUTOR", head: { sha: "no-author-head-sha", ref: "feature/checkout-retry" }, labels: [], body: "x" }); + await upsertPullRequestFile(env, { repoFullName, pullNumber: 4214, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); + await upsertRepoFocusManifest(env, repoFullName, { features: { e2eTests: true }, review: { e2e_test_delivery: "commit" } }); + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.endsWith("/pulls/4214") && method === "GET") return Response.json({ head: { ref: "feature/checkout-retry", sha: "no-author-head-sha", repo: { full_name: repoFullName } } }); + if (url.endsWith("/git/commits/no-author-head-sha") && method === "GET") return Response.json({ tree: { sha: "base-tree-sha" } }); + if (url.endsWith("/git/trees") && method === "POST") return Response.json({ sha: "new-tree-sha" }); + if (url.endsWith("/git/commits") && method === "POST") return Response.json({ sha: "no-author-commit-sha" }); + if (method === "PATCH") return Response.json({}); + if (url.includes("/issues/4214/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4214/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42140 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4214, "maintainer", { association: "MEMBER" })); + + expect(postedBody).toContain("pushed as a commit"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "committed" }); + }); + + it("respects agentDryRun — never attempts commit delivery, and records dry_run (not agent_paused)", async () => { + const repoFullName = "JSONbored/gen-tests-4197-commit-dryrun"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedGenerateTestsPr(env, repoFullName, 4211, "dryrun-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory", agentDryRun: true }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4211, "maintainer", { association: "MEMBER" })); + + expect(run).not.toHaveBeenCalled(); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("dry_run"); + const generated = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ id: string }>(); + expect(generated ?? null).toBeNull(); + }); + + it("respects agentPaused — never attempts generation or commit delivery, and records agent_paused", async () => { + const repoFullName = "JSONbored/gen-tests-4197-commit-paused"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedGenerateTestsPr(env, repoFullName, 4212, "paused-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory", agentPaused: true }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 4212, "maintainer", { association: "MEMBER" })); + + expect(run).not.toHaveBeenCalled(); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("agent_paused"); + }); + }); + }); + + // #4196 (part of the #4189 epic): promotes the existing manifest_missing_tests advisory finding into an + // actual auto-trigger for #4192/#4194's generation-and-render path, additive to the explicit + // `@gittensory generate-tests` command (#4195) tested above -- this describe block drives the AUTOMATED + // review pass (maybePublishPrPublicSurface, via a `pull_request` webhook) rather than an issue_comment. + describe("manifest_missing_tests auto-trigger (#4196)", () => { + const AUTO_TEST_SOURCE = "import { test, expect } from '@playwright/test';\n\ntest('auto-generated coverage', async ({ page }) => {\n await page.goto('/');\n await expect(page).toHaveTitle(/./);\n});"; + + async function seedAutoTriggerPr( + env: Env, + repoFullName: string, + prNumber: number, + headSha: string, + opts: { e2eTests?: boolean; hasTestFile?: boolean; validationNote?: boolean; manifestPolicyGateMode?: "advisory" | "block"; e2eTestDelivery?: "comment" | "commit"; autoTrigger?: boolean } = {}, + ) { + const slash = repoFullName.indexOf("/"); + const owner = repoFullName.slice(0, slash); + const name = repoFullName.slice(slash + 1); + await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); + await upsertRepositorySettings(env, { + repoFullName, + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + // reviewCheckMode: "required" (not "disabled") -- gateEnabled (which the whole manifestPolicyGateMode + // block this auto-trigger lives inside is downstream of) requires a truthy reviewCheckMode + a headSha. + // With reviewCheckMode: "disabled" the function bails out via its own early-return before ever reaching + // guidance. + gateCheckMode: "enabled", reviewCheckMode: "required", + requireLinkedIssue: false, + linkedIssueGateMode: "off", + manifestPolicyGateMode: opts.manifestPolicyGateMode ?? "advisory", + aiReviewMode: "off", + typeLabelsEnabled: false, + }); + await upsertPullRequestFromGitHub(env, repoFullName, { + number: prNumber, + title: "Add retry to checkout", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: headSha, ref: "feature/checkout-retry" }, + labels: [], + body: opts.validationNote ? "Ran npm run test:ci -- all green." : "No validation evidence mentioned here.", + }); + await upsertPullRequestFile(env, { + repoFullName, + pullNumber: prNumber, + path: opts.hasTestFile ? "test/unit/checkout.test.ts" : "src/checkout.ts", + status: "modified", + additions: 3, + deletions: 0, + changes: 3, + payload: { patch: "+function retryPayment() {\n+ return true;\n+}" }, + }); + // testExpectations is a TOP-LEVEL manifest field (unlike review.e2e_test_delivery's nested snake_case) -- + // both it and features.e2eTests must land in the SAME upsertRepoFocusManifest call, since a second + // separate call replaces rather than merges with the first. + // autoTrigger defaults to true here (NOT the production default) since this whole describe block exists + // to exercise the auto-trigger's own behavior -- the one test that cares about the real production + // default (OFF) passes `autoTrigger: false` explicitly, mirroring how the `e2eTests: false` case above + // already tests ITS OWN negative default the same way. + await upsertRepoFocusManifest(env, repoFullName, { + testExpectations: ["Run npm run test:ci."], + features: { e2eTests: opts.e2eTests ?? true }, + review: { e2e_test_auto_trigger: opts.autoTrigger ?? true, ...(opts.e2eTestDelivery ? { e2e_test_delivery: opts.e2eTestDelivery } : {}) }, + }); + } + + const autoTriggerWebhook = (repoFullName: string, prNumber: number, headSha: string, action: "opened" | "synchronize" = "opened", body = "No validation evidence mentioned here.") => ({ + type: "github-webhook" as const, + deliveryId: `auto-e2e-${prNumber}-${headSha}-${action}`, + eventName: "pull_request" as const, + payload: { + action, + installation: { id: 123, account: { login: repoFullName.slice(0, repoFullName.indexOf("/")), id: 1, type: "User" } }, + repository: { name: repoFullName.slice(repoFullName.indexOf("/") + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, repoFullName.indexOf("/")) } }, + pull_request: { + number: prNumber, + title: "Add retry to checkout", + state: "open", + user: { login: "contributor" }, + head: { sha: headSha }, + labels: [], + // The incoming webhook payload's own body ALWAYS re-upserts the cached PR record before this pass + // runs, overwriting whatever body seedAutoTriggerPr wrote directly to the DB -- so a test that needs + // a specific validation-note body must pass it here, not rely on the DB seed alone. + body, + }, + }, + }) as unknown as Parameters[1]; + + function stubAutoTriggerFetch(prNumber: number, posted: { count: number; body: string }) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // gateCheckMode: "enabled" means this pass ALSO publishes/updates a gate check-run -- these three + // endpoints back that unrelated publish, not the e2e-test-gen comment itself. + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: prNumber * 100 }, { status: 201 }); + if (url.includes("/check-runs") && method === "PATCH") return Response.json({ id: prNumber * 100, html_url: `https://github.com/checks/${prNumber * 100}` }); + if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { + posted.count += 1; + posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: prNumber * 10 }); + } + return new Response("not found", { status: 404 }); + }); + } + + it("auto-triggers generation when manifest_missing_tests fires and features.e2eTests is enabled", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-ok"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedAutoTriggerPr(env, repoFullName, 5001, "auto-4196-ok-sha"); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5001, posted); + + await processJob(env, autoTriggerWebhook(repoFullName, 5001, "auto-4196-ok-sha")); + + expect(posted.count).toBe(1); + expect(posted.body).toContain("test('auto-generated coverage'"); + const audited = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ outcome: string; metadata_json: string }>(); + expect(audited?.outcome).toBe("completed"); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ trigger: "auto", headSha: "auto-4196-ok-sha" }); + }); + + it("keeps the automated manifest_missing_tests trigger comment-only even when the manifest opts explicit commands into commit delivery", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-commit-forced-comment"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedAutoTriggerPr(env, repoFullName, 5011, "auto-4196-commit-forced-comment-sha", { e2eTestDelivery: "commit" }); + const posted = { count: 0, body: "" }; + const gitWrites: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 501100 }, { status: 201 }); + if (url.includes("/check-runs") && method === "PATCH") return Response.json({ id: 501100, html_url: "https://github.com/checks/501100" }); + if (url.includes("/git/trees") || url.includes("/git/commits") || url.includes("/git/refs/")) { + gitWrites.push(`${method} ${url}`); + return new Response("unexpected git write", { status: 500 }); + } + if (url.includes("/issues/5011/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/5011/comments") && method === "POST") { + posted.count += 1; + posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 50110 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, autoTriggerWebhook(repoFullName, 5011, "auto-4196-commit-forced-comment-sha")); + + expect(gitWrites).toEqual([]); + expect(posted.count).toBe(1); + expect(posted.body).toContain("test('auto-generated coverage'"); + const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "comment", trigger: "auto" }); + }); + + it("does not auto-trigger when manifest_missing_tests fires but features.e2eTests is disabled for the repo", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-disabled"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedAutoTriggerPr(env, repoFullName, 5002, "auto-4196-disabled-sha", { e2eTests: false }); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5002, posted); + + await processJob(env, autoTriggerWebhook(repoFullName, 5002, "auto-4196-disabled-sha")); + + expect(run).not.toHaveBeenCalled(); + expect(posted.count).toBe(0); + const audited = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ n: number }>(); + expect(audited?.n).toBe(0); + }); + + it("does not auto-trigger when features.e2eTests is enabled but review.e2e_test_auto_trigger is not set (safe default, #4196 separation)", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-no-opt-in"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + // e2eTests stays enabled (the master feature, which unlocks the command/checkbox) but autoTrigger is + // explicitly withheld -- the exact "enabled for maintainer-initiated use, but never fires unprompted" + // shape the feature must default to. + await seedAutoTriggerPr(env, repoFullName, 5012, "auto-4196-no-opt-in-sha", { autoTrigger: false }); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5012, posted); + + await processJob(env, autoTriggerWebhook(repoFullName, 5012, "auto-4196-no-opt-in-sha")); + + expect(run).not.toHaveBeenCalled(); + expect(posted.count).toBe(0); + const audited = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); + expect(audited?.n).toBe(0); + }); + + it("does not auto-trigger when the PR already carries a test file (the manifest_missing_tests signal never fires)", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-has-test"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedAutoTriggerPr(env, repoFullName, 5003, "auto-4196-has-test-sha", { hasTestFile: true }); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5003, posted); + + await processJob(env, autoTriggerWebhook(repoFullName, 5003, "auto-4196-has-test-sha")); + + expect(run).not.toHaveBeenCalled(); + expect(posted.count).toBe(0); + }); + + it("does not auto-trigger when the PR body already carries a validation note (the manifest_missing_tests signal never fires)", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-validated"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedAutoTriggerPr(env, repoFullName, 5004, "auto-4196-validated-sha", { validationNote: true }); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5004, posted); + + await processJob(env, autoTriggerWebhook(repoFullName, 5004, "auto-4196-validated-sha", "opened", "Ran npm run test:ci -- all green.")); + + expect(run).not.toHaveBeenCalled(); + expect(posted.count).toBe(0); + }); + + it("does not re-trigger generation on a second automated pass over the SAME unchanged head SHA (double-generation guard)", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-dedup"; + let runCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { runCalls += 1; return { response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }; } } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedAutoTriggerPr(env, repoFullName, 5005, "auto-4196-dedup-sha"); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5005, posted); + + // Two passes over the identical head SHA -- e.g. a `synchronize` redelivery or a re-review sweep tick + // with no new push in between. + await processJob(env, autoTriggerWebhook(repoFullName, 5005, "auto-4196-dedup-sha", "opened")); + await processJob(env, autoTriggerWebhook(repoFullName, 5005, "auto-4196-dedup-sha", "synchronize")); + + expect(runCalls).toBe(1); + expect(posted.count).toBe(1); + const rows = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ n: number }>(); + expect(rows?.n).toBe(1); + }); + + it("DOES trigger again for a genuinely NEW head SHA (a real push) even though a prior SHA on the same PR already fired", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-new-push"; + let runCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { runCalls += 1; return { response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }; } } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedAutoTriggerPr(env, repoFullName, 5006, "auto-4196-first-sha"); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5006, posted); + await processJob(env, autoTriggerWebhook(repoFullName, 5006, "auto-4196-first-sha", "opened")); + expect(runCalls).toBe(1); + + // A genuine new push: the PR's cached head SHA moves, re-seeding the manifest (features.e2eTests stays + // on) and re-running the webhook at the NEW sha. + await upsertPullRequestFromGitHub(env, repoFullName, { number: 5006, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "auto-4196-second-sha", ref: "feature/checkout-retry" }, labels: [], body: "No validation evidence mentioned here." }); + await processJob(env, autoTriggerWebhook(repoFullName, 5006, "auto-4196-second-sha", "synchronize")); + + expect(runCalls).toBe(2); + expect(posted.count).toBe(2); + }); + + it("an explicit @gittensory generate-tests command still regenerates on the SAME head SHA the auto-trigger already covered", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-explicit-after-auto"; + let runCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { runCalls += 1; return { response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }; } } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedAutoTriggerPr(env, repoFullName, 5007, "auto-4196-explicit-sha"); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5007, posted); + await processJob(env, autoTriggerWebhook(repoFullName, 5007, "auto-4196-explicit-sha")); + expect(runCalls).toBe(1); + + // Now the maintainer explicitly asks, on the SAME PR at the SAME (still-unpushed) head SHA. The + // auto-trigger's dedup guard must not leak into the explicit command's own path. + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/5007/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/5007/comments") && method === "POST") { posted.count += 1; posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 50070 }); } + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "auto-e2e-4196-explicit-command", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "auto-e2e-4196-explicit-after-auto", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 5007, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 50071, body: "@gittensory generate-tests", author_association: "MEMBER", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + } as unknown as Parameters[1]); + + expect(runCalls).toBe(2); + expect(posted.count).toBe(2); + const rows = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ n: number }>(); + expect(rows?.n).toBe(2); + }); + + it("respects agentPaused — records a skip and never spends an LLM call, even though the signal fired", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-paused"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedAutoTriggerPr(env, repoFullName, 5008, "auto-4196-paused-sha"); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off", agentPaused: true }); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5008, posted); + + await processJob(env, autoTriggerWebhook(repoFullName, 5008, "auto-4196-paused-sha")); + + expect(run).not.toHaveBeenCalled(); + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("agent_paused"); + }); + + it("respects agentDryRun — records a skip with detail dry_run (not agent_paused), and never spends an LLM call", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-dryrun"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedAutoTriggerPr(env, repoFullName, 5009, "auto-4196-dryrun-sha"); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off", agentDryRun: true }); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5009, posted); + + await processJob(env, autoTriggerWebhook(repoFullName, 5009, "auto-4196-dryrun-sha")); + + expect(run).not.toHaveBeenCalled(); + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("dry_run"); + }); + + it("attributes the generated test to \"the PR author\" when the cached PR has no author login at all (a ghost/deleted account)", async () => { + const repoFullName = "JSONbored/auto-e2e-4196-no-author"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off" }); + // Deliberately no `user` field at all -- authorLogin resolves to null, exercising the `author ?? "the PR + // author"` fallback arm (the explicit command's own `actor` is always a real commenter login, so this + // branch is reachable only from the auto-trigger, which has no comment-invoker to fall back on). + await upsertPullRequestFromGitHub(env, repoFullName, { number: 5010, title: "Add retry to checkout", state: "open", author_association: "CONTRIBUTOR", head: { sha: "auto-4196-no-author-sha", ref: "feature/checkout-retry" }, labels: [], body: "No validation evidence mentioned here." }); + await upsertPullRequestFile(env, { repoFullName, pullNumber: 5010, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); + await upsertRepoFocusManifest(env, repoFullName, { testExpectations: ["Run npm run test:ci."], features: { e2eTests: true }, review: { e2e_test_auto_trigger: true } }); + const posted = { count: 0, body: "" }; + stubAutoTriggerFetch(5010, posted); + + // Built inline (not via autoTriggerWebhook) so the incoming payload's own pull_request sub-object omits + // `user` too -- autoTriggerWebhook always hardcodes a real `user.login`, which would re-upsert (and thus + // restore) an author login before this pass ever runs. + await processJob(env, { + type: "github-webhook", + deliveryId: "auto-e2e-4196-no-author", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "auto-e2e-4196-no-author", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 5010, title: "Add retry to checkout", state: "open", head: { sha: "auto-4196-no-author-sha" }, labels: [], body: "No validation evidence mentioned here." }, + }, + } as unknown as Parameters[1]); + + expect(posted.count).toBe(1); + expect(posted.body).toContain("AI-generated Playwright test for @the PR author"); + }); + }); + + // #4589: the interactive counterpart to #4583's text-only CTA. Same issue_comment.edited detection shell as + // the pre-existing "PR-panel retrigger" checkbox (marker presence, bot's-own-comment confirmation, bot-sender + // guard, payload.sender as the real actor re-authorized server-side), but dispatches through the SAME shared + // runE2eTestGenerationAndDeliver core the command (#4195) and auto-trigger (#4196) above already use. + describe("PR-panel generate-tests checkbox (#4589)", () => { + const CHECKBOX_TEST_SOURCE = "import { test, expect } from '@playwright/test';\n\ntest('checkbox-generated coverage', async ({ page }) => {\n await page.goto('/');\n await expect(page).toHaveTitle(/./);\n});"; + + async function seedCheckboxPr( + env: Env, + repoFullName: string, + prNumber: number, + headSha: string, + opts: { e2eTests?: boolean } = {}, + ) { + const slash = repoFullName.indexOf("/"); + const owner = repoFullName.slice(0, slash); + const name = repoFullName.slice(slash + 1); + await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); + await upsertRepositorySettings(env, { + repoFullName, + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + requireLinkedIssue: false, + linkedIssueGateMode: "off", + manifestPolicyGateMode: "advisory", + aiReviewMode: "off", + typeLabelsEnabled: false, + }); + await upsertPullRequestFromGitHub(env, repoFullName, { + number: prNumber, + title: "Add retry to checkout", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: headSha, ref: "feature/checkout-retry" }, + labels: [], + body: "No validation evidence mentioned here.", + }); + await upsertPullRequestFile(env, { + repoFullName, + pullNumber: prNumber, + path: "src/checkout.ts", + status: "modified", + additions: 3, + deletions: 0, + changes: 3, + payload: { patch: "+function retryPayment() {\n+ return true;\n+}" }, + }); + await upsertRepoFocusManifest(env, repoFullName, { + testExpectations: ["Run npm run test:ci."], + features: { e2eTests: opts.e2eTests ?? true }, + }); + } + + const CHECKED_GENERATE_TESTS_PANEL = [ + "", + "", + "- [x] Generate an AI Playwright test for this PR", + ].join("\n"); + + function checkboxWebhook( + repoFullName: string, + prNumber: number, + commentId: number, + sender: { login: string; type?: "User" | "Bot" }, + opts: { body?: string; commentUser?: { login: string; type: "User" | "Bot" }; omitInstallation?: boolean; omitPullRequest?: boolean } = {}, + ) { + const slash = repoFullName.indexOf("/"); + return { + type: "github-webhook" as const, + deliveryId: `checkbox-${prNumber}-${commentId}`, + eventName: "issue_comment" as const, + payload: { + action: "edited", + ...(opts.omitInstallation ? {} : { installation: { id: 123, account: { login: repoFullName.slice(0, slash), id: 1, type: "User" } } }), + repository: { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, + issue: { number: prNumber, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, ...(opts.omitPullRequest ? {} : { pull_request: {} }) }, + comment: { id: commentId, body: opts.body ?? CHECKED_GENERATE_TESTS_PANEL, user: opts.commentUser ?? { login: "gittensory[bot]", type: "Bot" } }, + sender: { login: sender.login, type: sender.type ?? "User" }, + }, + } as unknown as Parameters[1]; + } + + function stubCheckboxFetch(prNumber: number, actorLogin: string, permission: string, posted: { count: number; body: string }) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes(`/collaborators/${actorLogin}/permission`)) return Response.json({ permission }); + if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { + posted.count += 1; + posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: prNumber * 10 }); + } + return new Response("not found", { status: 404 }); + }); + } + + it("dispatches generation when a maintainer checks the box", async () => { + const repoFullName = "JSONbored/checkbox-4589-ok"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + CHECKBOX_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedCheckboxPr(env, repoFullName, 6001, "checkbox-4589-ok-sha"); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6001, "maintainer", "admin", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6001, 900, { login: "maintainer" })); + + expect(posted.count).toBe(1); + expect(posted.body).toContain("test('checkbox-generated coverage'"); + const audited = await env.DB.prepare("select outcome, actor, metadata_json from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation") + .first<{ outcome: string; actor: string; metadata_json: string }>(); + expect(audited?.outcome).toBe("completed"); + expect(audited?.actor).toBe("maintainer"); + expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ trigger: "checkbox" }); + }); + + it("is a silent no-op when a non-maintainer checks the box — no comment posted, only a denial audit event", async () => { + const repoFullName = "JSONbored/checkbox-4589-denied"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: vi.fn() } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedCheckboxPr(env, repoFullName, 6002, "checkbox-4589-denied-sha"); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6002, "drive-by-user", "read", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6002, 901, { login: "drive-by-user" })); + + expect(posted.count).toBe(0); + const denied = await env.DB.prepare("select actor, outcome, detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_denied") + .first<{ actor: string; outcome: string; detail: string }>(); + expect(denied).toMatchObject({ actor: "drive-by-user", outcome: "denied" }); + }); + + // Authorization used to be hardcoded to maintainer-only here, ignoring whatever a repo's own + // .gittensory.yml commandAuthorization configured -- a self-hoster who wants their contributors to be + // able to trigger test generation had no way to widen it. It now respects settings.commandAuthorization, + // the exact same resolved (and safely clamped) policy the text-command version already uses. + it("dispatches generation for a COLLABORATOR (not just a maintainer) once the repo widens commandAuthorization for generate-tests", async () => { + const repoFullName = "JSONbored/checkbox-4589-widened"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + CHECKBOX_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedCheckboxPr(env, repoFullName, 6013, "checkbox-4589-widened-sha"); + await upsertRepositorySettings(env, { + repoFullName, + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + requireLinkedIssue: false, + linkedIssueGateMode: "off", + manifestPolicyGateMode: "advisory", + aiReviewMode: "off", + commandAuthorization: { default: ["maintainer"], commands: { "generate-tests": ["maintainer", "collaborator"] } }, + }); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6013, "collab-user", "write", posted); // "write" permission resolves to the COLLABORATOR association + + await processJob(env, checkboxWebhook(repoFullName, 6013, 911, { login: "collab-user" })); + + expect(posted.count).toBe(1); + expect(posted.body).toContain("test('checkbox-generated coverage'"); + const audited = await env.DB.prepare("select outcome, actor from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation") + .first<{ outcome: string; actor: string }>(); + expect(audited).toMatchObject({ outcome: "completed", actor: "collab-user" }); + }); + + it("still denies the PR's own author even if the repo tries to configure the raw pr_author role for generate-tests (safety clamp holds)", async () => { + const repoFullName = "JSONbored/checkbox-4589-clamped"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: vi.fn() } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + // seedCheckboxPr's own PR fixture is authored by "contributor" -- the SAME login checks the box below. + await seedCheckboxPr(env, repoFullName, 6014, "checkbox-4589-clamped-sha"); + await upsertRepositorySettings(env, { + repoFullName, + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "off", + requireLinkedIssue: false, + linkedIssueGateMode: "off", + manifestPolicyGateMode: "advisory", + aiReviewMode: "off", + // A repo attempting to grant its own PR authors unconditional access -- normalizeCommandRoleList drops + // the spoofable raw pr_author role for any MAINTAINER_ONLY_DEFAULT_COMMANDS entry (generate-tests is + // one), re-clamped at the point of use regardless of what's stored here. + commandAuthorization: { default: ["maintainer"], commands: { "generate-tests": ["pr_author"] } }, + }); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6014, "contributor", "read", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6014, 912, { login: "contributor" })); + + expect(posted.count).toBe(0); + const denied = await env.DB.prepare("select actor, outcome from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_denied") + .first<{ actor: string; outcome: string }>(); + expect(denied).toMatchObject({ actor: "contributor", outcome: "denied" }); + }); + + it("skips a bot-initiated edit (the bot's own comment re-render) without dispatching generation", async () => { + const repoFullName = "JSONbored/checkbox-4589-bot"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedCheckboxPr(env, repoFullName, 6003, "checkbox-4589-bot-sha"); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6003, "gittensory[bot]", "admin", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6003, 902, { login: "gittensory[bot]", type: "Bot" })); + + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe("bot_author"); + }); + + it("ignores the marker when it appears in a comment that isn't the bot's own", async () => { + const repoFullName = "JSONbored/checkbox-4589-not-bot-comment"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedCheckboxPr(env, repoFullName, 6009, "checkbox-4589-not-bot-comment-sha"); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6009, "maintainer", "admin", posted); + + await processJob( + env, + checkboxWebhook(repoFullName, 6009, 908, { login: "maintainer" }, { commentUser: { login: "someone-else", type: "User" } }), + ); + + expect(posted.count).toBe(0); + const events = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); + expect(events?.n).toBe(0); + }); + + it("skips when features.e2eTests is disabled for the repo, even though the checkbox was checked", async () => { + const repoFullName = "JSONbored/checkbox-4589-disabled"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: vi.fn() } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedCheckboxPr(env, repoFullName, 6004, "checkbox-4589-disabled-sha", { e2eTests: false }); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6004, "maintainer", "admin", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6004, 903, { login: "maintainer" })); + + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe("feature_disabled"); + }); + + it("ignores an edit where the generate-tests marker isn't checked (e.g. only the re-run box was checked)", async () => { + const repoFullName = "JSONbored/checkbox-4589-other-marker"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedCheckboxPr(env, repoFullName, 6005, "checkbox-4589-other-marker-sha"); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6005, "maintainer", "admin", posted); + const otherPanel = [ + "", + "", + "- [x] Re-run Gittensory review", + "- [ ] Generate an AI Playwright test for this PR", + ].join("\n"); + + await processJob(env, checkboxWebhook(repoFullName, 6005, 904, { login: "maintainer" }, { body: otherPanel })); + + expect(posted.count).toBe(0); + const events = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); + expect(events?.n).toBe(0); + }); + + it("skips a malformed payload (no installation / not a PR comment) without throwing", async () => { + const repoFullName = "JSONbored/checkbox-4589-malformed"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedCheckboxPr(env, repoFullName, 6006, "checkbox-4589-malformed-sha"); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6006, "maintainer", "admin", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6006, 905, { login: "maintainer" }, { omitPullRequest: true })); + + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe("missing_repo_pr_or_installation"); + }); + + it("skips when the cached PR record is missing", async () => { + const repoFullName = "JSONbored/checkbox-4589-no-pr"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + // Repo registered but NO PR ever upserted -- getPullRequest resolves null. + await upsertRepositoryFromGitHub(env, { name: "checkbox-4589-no-pr", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, 123); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6007, "maintainer", "admin", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6007, 906, { login: "maintainer" })); + + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe("cached_pr_missing"); + }); + + it("respects agentPaused — records a skip and never spends an LLM call, even though an authorized maintainer checked the box", async () => { + const repoFullName = "JSONbored/checkbox-4589-paused"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedCheckboxPr(env, repoFullName, 6008, "checkbox-4589-paused-sha"); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "off", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off", agentPaused: true }); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6008, "maintainer", "admin", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6008, 907, { login: "maintainer" })); + + expect(run).not.toHaveBeenCalled(); + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe("agent_paused"); + }); + + it("respects agentDryRun — records a skip with detail dry_run (not agent_paused)", async () => { + const repoFullName = "JSONbored/checkbox-4589-dryrun"; + const run = vi.fn(); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); + await seedCheckboxPr(env, repoFullName, 6010, "checkbox-4589-dryrun-sha"); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "off", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off", agentDryRun: true }); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6010, "maintainer", "admin", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6010, 909, { login: "maintainer" })); + + expect(run).not.toHaveBeenCalled(); + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe("dry_run"); + }); + + it("respects the repo's configured commit delivery mode via the checkbox (NOT forced comment-only, unlike the auto-trigger)", async () => { + const repoFullName = "JSONbored/checkbox-4589-commit"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + CHECKBOX_TEST_SOURCE + "\n```" }) } as unknown as Ai, + GITTENSORY_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedCheckboxPr(env, repoFullName, 6011, "checkbox-4589-commit-sha"); + await upsertRepoFocusManifest(env, repoFullName, { + testExpectations: ["Run npm run test:ci."], + features: { e2eTests: true }, + review: { e2e_test_delivery: "commit" }, + }); + const posted = { count: 0, body: "" }; + const gitWrites: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.endsWith("/pulls/6011") && method === "GET") { + return Response.json({ head: { ref: "feature/checkout-retry", sha: "checkbox-4589-commit-sha", repo: { full_name: repoFullName } } }); + } + if (url.endsWith("/git/commits/checkbox-4589-commit-sha") && method === "GET") return Response.json({ tree: { sha: "base-tree" } }); + if (url.endsWith("/git/trees") && method === "POST") { + gitWrites.push("tree"); + return Response.json({ sha: "new-tree" }); + } + if (url.endsWith("/git/commits") && method === "POST") { + gitWrites.push("commit"); + return Response.json({ sha: "new-commit" }); + } + if (method === "PATCH") { + gitWrites.push("ref"); + return Response.json({}); + } + if (url.includes("/issues/6011/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/6011/comments") && method === "POST") { + posted.count += 1; + posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 60110 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, checkboxWebhook(repoFullName, 6011, 910, { login: "maintainer" })); + + expect(gitWrites).toEqual(["tree", "commit", "ref"]); + expect(posted.count).toBe(1); + expect(posted.body).toContain("pushed as a commit"); + }); + + it("renders the checkbox (and the Test coverage collapsible) in the main review comment for a detected contributor missing tests", async () => { + const repoFullName = "JSONbored/checkbox-4589-full-panel"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_E2E_TESTS: "true", + // The checkbox/collapsible only render via the CONVERGED comment builder (buildUnifiedCommentBody); + // the legacy buildPublicPrIntelligenceComment path has neither and must be opted out of here too. + GITTENSORY_REVIEW_UNIFIED_COMMENT: "true", + }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName, + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + // gateCheckMode MUST be "enabled" (not "off") -- maybePublishPrPublicSurface only takes the UNIFIED + // renderer branch when BOTH unifiedCommentAllowed AND gateEvaluation are truthy; gateEvaluation is + // never computed at all when the gate is off, silently falling back to the legacy panel (which has + // neither the Test coverage collapsible nor the generate-tests checkbox). Mirrors the settings shape + // of the pre-existing "renders the unified PR-review comment..." test above. + gateCheckMode: "enabled", reviewCheckMode: "required", + requireLinkedIssue: false, + linkedIssueGateMode: "off", + manifestPolicyGateMode: "advisory", + aiReviewMode: "off", + typeLabelsEnabled: false, + }); + await upsertRepoFocusManifest(env, repoFullName, { testExpectations: ["Run npm run test:ci."], features: { e2eTests: true, unifiedComment: true } }); + // gateEvaluation needs a resolved CI aggregate (mocking the module function directly is far simpler than + // stubbing every raw status/check-suite endpoint the live CI aggregator would otherwise call) -- but + // NOT "passed": resolveManifestPassedValidationCount treats a fully-green live CI rollup as validation + // evidence in its own right (`liveCi.ciState === "passed" ? 1 : 0`), which would satisfy + // manifest_missing_tests's own passedValidationCount check and suppress the very finding this test needs + // to fire. "pending" still lets the gate resolve a verdict without smuggling in validation evidence. + const liveCiSpy = vi.spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl").mockResolvedValue({ + ciState: "pending", + hasPending: true, + hasVisiblePending: true, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + const posted = { count: 0, body: "" }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") + return Response.json([ + { uid: 9, githubUsername: "contributor", githubId: "321", totalPrs: 5, totalMergedPrs: 4, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, + ]); + if (url === "https://api.gittensor.io/miners/321") return Response.json({ repositories: [{ repositoryFullName: repoFullName, totalPrs: "5", totalMergedPrs: "4", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); + if (url === "https://api.gittensor.io/miners/321/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/321/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); + if (url.includes("/users/contributor/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/6012/files")) return Response.json([{ filename: "src/checkout.ts", additions: 3, deletions: 0, status: "modified" }]); + if (/\/pulls\/6012(?:\?|$)/.test(url)) return Response.json({ number: 6012, mergeable_state: "clean" }); + // Gate check-run — must succeed so gateEvaluation is produced and the unified-renderer branch runs. + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 950 }, { status: 201 }); + if (url.includes("/check-runs/950") && method === "PATCH") return Response.json({ id: 950 }); + // Stateful comment store (mirrors the retrigger tests' own GET-finds-the-prior-POST pattern): the + // FIRST GET finds nothing (posts a fresh comment), every SUBSequent GET/PATCH finds and updates the + // SAME row -- a stub that always returns [] on GET would make the code re-POST on every update + // attempt instead of PATCHing, inflating posted.count for reasons unrelated to this test. + if (url.includes(`/issues/6012/comments`) && method === "GET") { + return Response.json(posted.count > 0 ? [{ id: 60120, body: posted.body, user: { login: "gittensory[bot]", type: "Bot" } }] : []); + } + if (url.includes(`/issues/6012/comments`) && method === "POST") { + posted.count += 1; + posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 60120 }, { status: 201 }); + } + if (url.includes(`/issues/comments/60120`) && method === "PATCH") { + posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 60120 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "checkbox-4589-full-panel", + eventName: "pull_request", + payload: { + action: "opened", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 6012, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, head: { sha: "checkbox-4589-full-panel-sha" }, labels: [], body: "No validation evidence mentioned here." }, + }, + } as unknown as Parameters[1]); + + expect(liveCiSpy).toHaveBeenCalled(); + expect(posted.count).toBeGreaterThan(0); + expect(posted.body).toContain("
Test coverage"); + expect(posted.body).toContain("No changed test files or passing validation evidence were detected for this PR."); + expect(posted.body).toContain("- [ ] **[BETA]** Generate an AI Playwright test for this PR"); + }); + + it("handles a sparse payload with no repository, sender, or issue without throwing", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "checkbox-4589-sparse", + eventName: "issue_comment", + payload: { + action: "edited", + comment: { id: 999, body: CHECKED_GENERATE_TESTS_PANEL, user: { login: "gittensory[bot]", type: "Bot" } }, + sender: undefined, + }, + } as unknown as Parameters[1]), + ).resolves.not.toThrow(); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe("missing_repo_pr_or_installation"); + }); + + it("treats a non-Bot sender whose login merely ends in '[bot]' as a bot author (spoofing guard)", async () => { + const repoFullName = "JSONbored/checkbox-4589-bot-suffix"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedCheckboxPr(env, repoFullName, 6013, "checkbox-4589-bot-suffix-sha"); + const posted = { count: 0, body: "" }; + stubCheckboxFetch(6013, "impersonator[bot]", "admin", posted); + + await processJob(env, checkboxWebhook(repoFullName, 6013, 911, { login: "impersonator[bot]", type: "User" })); + + expect(posted.count).toBe(0); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.e2e_tests_generation_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe("bot_author"); + }); + }); + + it("ops-alerts job no-ops when GITTENSORY_REVIEW_OPS is OFF (does no anomaly scan)", async () => { + const env = createTestEnv(); // flag unset → OFF + await env.DB.prepare("INSERT INTO repositories (full_name, owner, name, is_installed, is_registered) VALUES (?, ?, ?, 1, 1)") + .bind("owner/repo", "owner", "repo") + .run(); + // Seed a gate false-positive anomaly that WOULD fire if the scan ran. + for (let i = 1; i <= 6; i += 1) { + await recordGateBlockOutcome(env, { repoFullName: "owner/repo", pullNumber: i, blockerCodes: ["missing_linked_issue"] }); + await upsertPullRequestFromGitHub(env, "owner/repo", { number: i, title: `PR ${i}`, state: "closed", merged_at: i <= 4 ? "2026-06-01T00:00:00.000Z" : null } as never); + } + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + await processJob(env, { type: "ops-alerts", requestedBy: "test" }); + expect(warn.mock.calls.map((c) => String(c[0])).some((line) => line.includes("ops_anomaly"))).toBe(false); + warn.mockRestore(); + }); + + it("ops-alerts job runs the anomaly scan when GITTENSORY_REVIEW_OPS is ON", async () => { + const env = createTestEnv({ GITTENSORY_REVIEW_OPS: "true" }); + await env.DB.prepare("INSERT INTO repositories (full_name, owner, name, is_installed, is_registered) VALUES (?, ?, ?, 1, 1)") + .bind("owner/repo", "owner", "repo") + .run(); + for (let i = 1; i <= 6; i += 1) { + await recordGateBlockOutcome(env, { repoFullName: "owner/repo", pullNumber: i, blockerCodes: ["missing_linked_issue"] }); + await upsertPullRequestFromGitHub(env, "owner/repo", { number: i, title: `PR ${i}`, state: "closed", merged_at: i <= 4 ? "2026-06-01T00:00:00.000Z" : null } as never); + } + const errors = vi.spyOn(console, "error").mockImplementation(() => {}); + await processJob(env, { type: "ops-alerts", requestedBy: "test" }); + expect(errors.mock.calls.map((c) => String(c[0])).some((line) => line.includes("ops_anomaly") && line.includes("owner/repo"))).toBe(true); + errors.mockRestore(); + }); + + it("sweep-liveness-watchdog job no-ops when GITTENSORY_SWEEP_WATCHDOG is OFF (does no scan, no re-enqueue)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); // flag unset → OFF + await upsertRepositoryFromGitHub(env, { name: "stale-repo", full_name: "owner/stale-repo", private: false, owner: { login: "owner" } }, 9310); + await upsertRepositorySettings(env, { repoFullName: "owner/stale-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/stale-repo", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); + + await processJob(env, { type: "sweep-liveness-watchdog", requestedBy: "test" }); + + expect(sent).toEqual([]); + }); + + it("sweep-liveness-watchdog job runs the liveness scan and re-enqueues a stale repo when GITTENSORY_SWEEP_WATCHDOG is ON", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ GITTENSORY_SWEEP_WATCHDOG: "true", JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertRepositoryFromGitHub(env, { name: "stale-repo", full_name: "owner/stale-repo", private: false, owner: { login: "owner" } }, 9311); + await upsertRepositorySettings(env, { repoFullName: "owner/stale-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/stale-repo", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); + + await processJob(env, { type: "sweep-liveness-watchdog", requestedBy: "test" }); + + expect(sent).toEqual([expect.objectContaining({ type: "agent-regate-sweep", repoFullName: "owner/stale-repo", installationId: 9311 })]); + }); + + it("reconcile-open-prs job no-ops when GITTENSORY_PR_RECONCILIATION is OFF (does no scan)", async () => { + const env = createTestEnv(); // flag unset → OFF + await upsertRepositoryFromGitHub(env, { name: "stale-repo", full_name: "owner/stale-repo", private: false, owner: { login: "owner" } }, 9410); + await upsertRepositorySettings(env, { repoFullName: "owner/stale-repo", autonomy: { merge: "auto" } }); + const reconcileSpy = vi.spyOn(backfillModule, "reconcileOpenPullRequests"); + + await processJob(env, { type: "reconcile-open-prs", requestedBy: "test" }); + + expect(reconcileSpy).not.toHaveBeenCalled(); + }); + + it("reconcile-open-prs job runs the reconciliation scan when GITTENSORY_PR_RECONCILIATION is ON", async () => { + const env = createTestEnv({ GITTENSORY_PR_RECONCILIATION: "true" }); + await upsertRepositoryFromGitHub(env, { name: "stale-repo", full_name: "owner/stale-repo", private: false, owner: { login: "owner" } }, 9411); + await upsertRepositorySettings(env, { repoFullName: "owner/stale-repo", autonomy: { merge: "auto" } }); + const reconcileSpy = vi.spyOn(backfillModule, "reconcileOpenPullRequests").mockResolvedValue({ repoFullName: "owner/stale-repo", remoteOpenCount: 0, localOpenCount: 0, missingNumbers: [] }); + + await processJob(env, { type: "reconcile-open-prs", requestedBy: "test" }); + + expect(reconcileSpy).toHaveBeenCalledWith(env, "owner/stale-repo"); + reconcileSpy.mockRestore(); + }); + + describe("type label decoupling (#label-decoupling)", () => { + function stubTypeLabelFetch(prNumber: number, seen: { posted: string[]; removed: string[]; checkRunCreated: boolean }) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes(`/commits/`) && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + seen.checkRunCreated = true; + return Response.json({ id: 9001 }, { status: 201 }); + } + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 9001 }); + if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { + seen.posted.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.includes(`/issues/${prNumber}/labels/`) && method === "DELETE") { + seen.removed.push(decodeURIComponent(url.split(`/issues/${prNumber}/labels/`)[1] ?? "")); + return new Response(null, { status: 204 }); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); + if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + } + + // Fails only the ONE audit_events insert whose bound values include `needle` (e.g. a specific + // eventType), leaving every other audit write in the same job untouched -- a blanket "throw on any + // audit_events insert" (as the sibling #orb-ci-stuck-repeat fail-open tests use for a narrower job + // type) breaks unrelated earlier writes on the fuller pull_request webhook path used here. + function failAuditEventInsertsContaining(env: Env, needle: string) { + const realPrepare = env.DB.prepare.bind(env.DB); + env.DB.prepare = ((sql: string) => { + const statement = realPrepare(sql); + if (!/insert\s+into\s+["`]?audit_events["`]?/i.test(sql)) return statement; + return { + ...statement, + bind(...values: unknown[]) { + const bound = statement.bind(...(values as never[])); + if (!values.some((value) => typeof value === "string" && value.includes(needle))) return bound; + return { ...bound, run: () => Promise.reject(new Error("audit write failed")) }; + }, + }; + }) as typeof env.DB.prepare; + } + + it("applies the type label when oss_maintainer mode + an unconfirmed miner suppress the context label", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + publicAudienceMode: "oss_maintainer", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "not_found" }, 60_000); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(210, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-oss-maintainer-unconfirmed", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 210, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha210" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.posted).toEqual(["gittensor:bug"]); + expect(seen.removed.sort()).toEqual(["gittensor:feature", "gittensor:priority"]); + }); + + it("keeps gate-only gittensor_only type labels silent until miner confirmation", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + publicAudienceMode: "gittensor_only", + autoLabelEnabled: false, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false, minerList: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + seen.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes(`/commits/`) && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + seen.checkRunCreated = true; + return Response.json({ id: 9002 }, { status: 201 }); + } + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 9002 }); + if (url.includes(`/issues/218/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/218/labels`) && method === "POST") { + seen.posted.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.includes(`/issues/218/labels/`) && method === "DELETE") { + seen.removed.push(decodeURIComponent(url.split(`/issues/218/labels/`)[1] ?? "")); + return new Response(null, { status: 204 }); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-gittensor-only-gate-only-muted", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 218, title: "fix: gate-only silence", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha218" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.minerList).toBe(1); + expect(seen.checkRunCreated).toBe(true); + expect(seen.posted).toEqual([]); + expect(seen.removed).toEqual([]); + }); + + it("still mutes the type label when gittensor_only mode's non-confirmed-miner silence applies, even with the gate enabled", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_and_label", + publicAudienceMode: "gittensor_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + // The only difference from the pre-existing "keeps GitHub-history-only contributors quiet" test + // (which has the gate off, so it returns before ever reaching the type-label decision): with the + // gate ENABLED, the function does NOT bail out early, so this is the only path that actually + // exercises `decision.skipReason === "not_official_gittensor_miner"` at the type-label gate. + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "not_found" }, 60_000); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(217, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-gittensor-only-muted", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 217, title: "fix: gittensor_only silence", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha217" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.posted).toEqual([]); + expect(seen.removed).toEqual([]); + }); + + it("does not apply the type label when typeLabelsEnabled is false, in the same oss_maintainer + unconfirmed-miner scenario", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + publicAudienceMode: "oss_maintainer", + autoLabelEnabled: true, + typeLabelsEnabled: false, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "not_found" }, 60_000); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(211, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-disabled-oss-maintainer-unconfirmed", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 211, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha211" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.posted).toEqual([]); + expect(seen.removed).toEqual([]); + }); + + it("applies the type label to a maintainer-authored PR even though includeMaintainerAuthors excludes it from the public surface", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + includeMaintainerAuthors: false, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(212, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-maintainer-author", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 212, title: "fix: internal cleanup", state: "open", user: { login: "org-member" }, author_association: "MEMBER", head: { sha: "sha212" }, labels: [], body: "Internal." }, + }, + }); + + expect(seen.posted).toEqual(["gittensor:bug"]); + expect(seen.posted).not.toContain("gittensor"); + }); + + it("applies the type label to a bot-authored PR and keeps the three type labels mutually exclusive", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(213, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-bot-author", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 213, title: "feat: add retry backoff", state: "open", user: { login: "renovate[bot]", type: "Bot" }, head: { sha: "sha213" }, labels: [], body: "Automated." }, + }, + }); + + expect(seen.posted).toEqual(["gittensor:feature"]); + expect(seen.removed.sort()).toEqual(["gittensor:bug", "gittensor:priority"]); + }); + + it("cleans up an arbitrary configured custom category alongside bug/feature/priority (#label-modularity)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + // A self-host taxonomy well beyond the built-in bug/feature/priority triad (#label-modularity): + // `security` is a registered category with no title-classification rule of its own, so it is + // never CHOSEN here, but it must still be a cleanup CANDIDATE (never left dangling on a PR whose + // classification moved elsewhere) exactly like the built-in categories. + typeLabels: { bug: "gittensor:bug", feature: "gittensor:feature", priority: "gittensor:priority", security: "area:security" }, + }); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(218, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-custom-category-cleanup", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 218, title: "feat: add retry backoff", state: "open", user: { login: "renovate[bot]", type: "Bot" }, head: { sha: "sha218" }, labels: [], body: "Automated." }, + }, + }); + + expect(seen.posted).toEqual(["gittensor:feature"]); + expect(seen.removed.sort()).toEqual(["area:security", "gittensor:bug", "gittensor:priority"]); + }); + + it("applies the type label when publicSurface: comment_only makes the base context label structurally impossible", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "comment_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(214, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-comment-only-surface", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 214, title: "fix: comment-only regression", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha214" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.posted).toEqual(["gittensor:bug"]); + }); + + it("typeLabelsEnabled: false does not suppress the base context label for a confirmed contributor", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + typeLabelsEnabled: false, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(215, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-disabled-confirmed-contributor", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 215, title: "fix: confirmed contributor path", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha215" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.posted).toEqual(["gittensor"]); + }); + + it("posts the Gittensory Context check run independently of both label families being off, with zero label writes", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + typeLabelsEnabled: false, + checkRunMode: "enabled", + gateCheckMode: "off", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(216, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-checkrun-independent", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 216, title: "fix: check-run independence", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha216" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.checkRunCreated).toBe(true); + expect(seen.posted).toEqual([]); + expect(seen.removed).toEqual([]); + }); + + function stubPropagationFetch( + prNumber: number, + linkedIssueNumber: number, + seen: { posted: string[]; removed: string[]; issueFetches: number }, + linkedIssueResponse: () => Response, + ) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes(`/commits/`) && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 9001 }, { status: 201 }); + if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 9001 }); + if (url.endsWith(`/issues/${linkedIssueNumber}`) && method === "GET") { + seen.issueFetches += 1; + return linkedIssueResponse(); + } + if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); + if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { + seen.posted.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); + return Response.json([]); + } + if (url.includes(`/issues/${prNumber}/labels/`) && method === "DELETE") { + seen.removed.push(decodeURIComponent(url.split(`/issues/${prNumber}/labels/`)[1] ?? "")); + return new Response(null, { status: 204 }); + } + if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); + if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + } + + it("applies the configured priority label when a linked issue already carries the configured issue label (#priority-linked-issue-gate)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + linkedIssueLabelPropagation: { + enabled: true, + mode: "exclusive_type_label", + mappings: [{ issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: true }], + }, + }); + const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; + stubPropagationFetch(220, 1, seen, () => Response.json({ number: 1, state: "open", user: { login: "contributor" }, labels: ["gittensor:priority"] })); + + await processJob(env, { + type: "github-webhook", + deliveryId: "priority-propagation-applied", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 220, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha220" }, labels: [], body: "Fixes #1" }, + }, + }); + + // JSONbored/gittensory falls back to its own bundled manifest (GITTENSORY_REPO_FOCUS_MANIFEST_YAML) when + // no other manifest source responds, which REPLACES this test's DB-configured single-mapping override + // with its own bug/feature (exclusive) + priority (additive) mapping list -- so the linked issue's + // gittensor:priority label composes with the title-derived "fix" -> gittensor:bug, rather than replacing + // it. Priority is additive (not a type of its own; see resolvePrTypeLabel's composition fix), so bug + // still applies from the title and only feature (never matched) needs removing. + expect(seen.issueFetches).toBe(1); + expect(seen.posted).toEqual(["gittensor:bug", "gittensor:priority"]); + expect(seen.removed).toEqual(["gittensor:feature"]); + }); + + it("REGRESSION (#4528, PR #4494 shape): keeps the propagated labels on the PR's own merge-closed webhook, instead of falling back to the title guess", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "acme/widget", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "off", + reviewCheckMode: "disabled", + linkedIssueGateMode: "off", + aiReviewMode: "off", + // Real-world shape: the type-label decision runs regardless of the check-run/gate publish mode, but + // the SURROUNDING function only reaches that far for an already-closed PR when the agent layer is + // configured (autonomyNeedsGateEvaluation) -- an unconfigured repo's closed-PR pass has nothing else + // to do and bails before the label block. `label: "auto"` is the minimal opt-in that reproduces this + // without pulling in merge/close autonomy's own CI-wait/rebase machinery. + autonomy: { label: "auto" }, + linkedIssueLabelPropagation: { + enabled: true, + mode: "exclusive_type_label", + mappings: [ + { issueLabel: "gittensor:feature", prLabel: "gittensor:feature", removeOtherTypeLabels: true }, + { issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: false }, + ], + }, + }); + const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; + // The linked issue is CLOSED, at a timestamp at/after this PR's own merge -- GitHub's standard "Closes #N" + // auto-close, fired by this very merge. Title deliberately uses a verb ("fold") absent from the + // feature-action-verb whitelist, so a title-only fallback would misclassify this as gittensor:bug -- + // this only stays gittensor:feature/gittensor:priority if the merge-closed issue is still trusted. + stubPropagationFetch(4494, 4279, seen, () => + Response.json({ + number: 4279, + state: "closed", + closed_at: "2026-07-09T22:15:14Z", + user: { login: "contributor" }, + labels: ["gittensor:feature", "gittensor:priority"], + }), + ); + + await processJob(env, { + type: "github-webhook", + deliveryId: "merge-close-race-4528", + eventName: "pull_request", + payload: { + action: "closed", + installation: { id: 123, account: { login: "acme", id: 1, type: "User" } }, + repository: { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, + pull_request: { + number: 4494, + title: "feat(x): fold run-state into the status panel", + state: "closed", + merged_at: "2026-07-09T22:15:13Z", + user: { login: "contributor" }, + author_association: "NONE", + head: { sha: "sha4494" }, + labels: [], + body: "Closes #4279", + }, + }, + }); + + expect(seen.issueFetches).toBe(1); + expect(seen.posted.sort()).toEqual(["gittensor:feature", "gittensor:priority"]); + expect(seen.removed).toEqual(["gittensor:bug"]); + }); + + it("REGRESSION (#regression-safe-propagation, was: 'fails open to the normal title-based label'): skips the label decision entirely — never falls back to title — when the linked issue's fetch fails, leaving existing labels untouched", async () => { + // Before the fix, a fetch failure here fell through to the title guess and OVERWROTE whatever labels + // were already correct — the exact mechanism (an inconclusive recheck treated as a confirmed absence + // of propagation authority) that let a transient GitHub hiccup permanently strip a correctly propagated + // gittensor:feature/gittensor:priority label down to gittensor:bug (confirmed in production, PRs + // #4716/#4783 and 116 others in a 2-day sample). A fetch failure must now be a no-op, not a downgrade. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + linkedIssueLabelPropagation: { + enabled: true, + mode: "exclusive_type_label", + mappings: [{ issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: true }], + }, + }); + const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; + stubPropagationFetch(221, 1, seen, () => new Response("server error", { status: 500 })); + + await processJob(env, { + type: "github-webhook", + deliveryId: "priority-propagation-fetch-failed", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 221, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha221" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.issueFetches).toBe(1); + expect(seen.posted).toEqual([]); + expect(seen.removed).toEqual([]); + const events = await env.DB.prepare( + `select outcome, detail from audit_events where event_type = 'github_app.type_label_decision' and target_key = 'JSONbored/gittensory#221'`, + ).all(); + expect(events.results).toEqual([{ outcome: "denied", detail: "propagation_inconclusive" }]); + }); + + it("REGRESSION (#regression-safe-propagation): a second pass whose propagation recheck is inconclusive never clobbers a first pass's already-correct propagated labels", async () => { + // Reproduces the exact PR #4716/#4783 shape end-to-end: an EARLIER pass correctly propagates + // gittensor:feature/gittensor:priority from the linked issue, then a LATER pass (a webhook re-review, a + // sweep tick, or simply a second near-simultaneous delivery for the same merge) re-runs the same + // decision but this time the linked issue's fetch fails transiently. The later pass must leave the + // correct labels exactly as the first pass left them. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "acme/widget", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + linkedIssueLabelPropagation: { + enabled: true, + mode: "exclusive_type_label", + mappings: [{ issueLabel: "gittensor:feature", prLabel: "gittensor:feature", removeOtherTypeLabels: true }], + }, + }); + const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; + let issueShouldFail = false; + stubPropagationFetch(4716, 2216, seen, () => + issueShouldFail + ? new Response("server error", { status: 500 }) + : Response.json({ number: 2216, state: "open", user: { login: "contributor" }, labels: ["gittensor:feature"] }), + ); + // Each pass uses a DIFFERENT action/head SHA so the second is a genuinely fresh re-evaluation, not a + // same-head no-op the surface-publish guard would short-circuit before ever reaching the label block. + const webhookPayload = (action: "opened" | "synchronize", headSha: string) => ({ + action, + installation: { id: 123, account: { login: "acme", id: 1, type: "User" as const } }, + repository: { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, + pull_request: { number: 4716, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE" as const, head: { sha: headSha }, labels: [], body: "Closes #2216" }, + }); + + await processJob(env, { type: "github-webhook", deliveryId: "pass-1-correct", eventName: "pull_request", payload: webhookPayload("opened", "sha4716a") }); + expect(seen.posted).toEqual(["gittensor:feature"]); + expect(seen.removed.sort()).toEqual(["gittensor:bug", "gittensor:priority"]); + + issueShouldFail = true; + await processJob(env, { type: "github-webhook", deliveryId: "pass-2-inconclusive", eventName: "pull_request", payload: webhookPayload("synchronize", "sha4716b") }); + // No FURTHER posts/removes happened in pass 2 -- the correct labels from pass 1 are exactly as they were. + expect(seen.posted).toEqual(["gittensor:feature"]); + expect(seen.removed.sort()).toEqual(["gittensor:bug", "gittensor:priority"]); + }); + + it("REGRESSION (#regression-safe-propagation): a contended per-PR actuation lock skips the label decision entirely instead of racing the pass that already holds it", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + linkedIssueLabelPropagation: { + enabled: true, + mode: "exclusive_type_label", + mappings: [{ issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: true }], + }, + }); + const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; + stubPropagationFetch(223, 1, seen, () => Response.json({ number: 1, state: "open", user: { login: "contributor" }, labels: ["gittensor:priority"] })); + + // Simulates a concurrent pass (a sibling webhook delivery, or the sweep) already holding this exact + // PR's actuation lock when this pass reaches the type-label block. + const held = await claimPrActuationLock(env, "JSONbored/gittensory", 223); + expect(held.acquired).toBe(true); + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "priority-propagation-lock-contended", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 223, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha223" }, labels: [], body: "Fixes #1" }, + }, + }); + } finally { + await releasePrActuationLock(env, "JSONbored/gittensory", 223, held.ownerToken); + } + + // The fetch never even reaches the linked-issue check -- the lock is claimed BEFORE any propagation work. + expect(seen.issueFetches).toBe(0); + expect(seen.posted).toEqual([]); + expect(seen.removed).toEqual([]); + const events = await env.DB.prepare( + `select outcome, detail from audit_events where event_type = 'github_app.type_label_decision' and target_key = 'JSONbored/gittensory#223'`, + ).all(); + expect(events.results).toEqual([{ outcome: "denied", detail: "lock_contended" }]); + }); + + it("never fetches a linked issue and keeps normal behavior when propagation is left at its default (disabled) (#priority-linked-issue-gate)", async () => { + // Deliberately NOT "JSONbored/gittensory" (unlike its two sibling tests above): this repo's own + // `.gittensory.yml` now enables propagation for itself (#priority-linked-issue-gate-ownership + // dogfooding), and `resolveRepositorySettings` falls back to the bundled + // `GITTENSORY_REPO_FOCUS_MANIFEST_YAML` copy of it whenever a live manifest fetch is unavailable + // (`isGittensorySelfRepo`, `src/signals/focus-manifest-loader.ts`) -- exactly the case in this test's + // stubbed fetch. Using gittensory's own literal repo name here would make this "propagation is off by + // DEFAULT" test silently stop being a default-behavior test at all. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "acme/widget", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + // linkedIssueLabelPropagation intentionally omitted -- defaults to disabled. + }); + const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; + stubPropagationFetch(222, 1, seen, () => Response.json({ number: 1, state: "open", labels: ["gittensor:priority"] })); + + await processJob(env, { + type: "github-webhook", + deliveryId: "priority-propagation-disabled-noop", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "acme", id: 1, type: "User" } }, + repository: { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, + pull_request: { number: 222, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha222" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.issueFetches).toBe(0); + expect(seen.posted).toEqual(["gittensor:bug"]); + expect(seen.removed.sort()).toEqual(["gittensor:feature", "gittensor:priority"]); + }); + + it("records the audit event for a normal applied label decision (#label-decoupling audit)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(219, seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-recorded", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 219, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha219" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(seen.posted).toEqual(["gittensor:bug"]); + const labelEvent = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.type_label_decision", "JSONbored/gittensory#219") + .first<{ outcome: string; detail: string }>(); + expect(labelEvent?.outcome).toBe("completed"); + expect(labelEvent?.detail).toBe("applied labels: gittensor:bug"); + }); + + it("does not let a failing audit write stop label application (completed outcome, fail-open)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(220, seen); + failAuditEventInsertsContaining(env, "github_app.type_label_decision"); + + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-completed-audit-fail", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 220, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha220" }, labels: [], body: "Fixes #1" }, + }, + }); + + // The label application itself must complete even though its audit-event write threw. + expect(seen.posted).toEqual(["gittensor:bug"]); + }); + + it("does not let a failing audit write stop the decision when type labels are disabled (denied outcome, fail-open)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: true, + typeLabelsEnabled: false, + createMissingLabel: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "off", + aiReviewMode: "off", + }); + const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; + stubTypeLabelFetch(221, seen); + failAuditEventInsertsContaining(env, "github_app.type_label_decision"); + + // Fail-open: the webhook job must still complete (and still reach the type-label decision) even + // though recording it fails. + await processJob(env, { + type: "github-webhook", + deliveryId: "type-label-denied-audit-fail", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 221, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha221" }, labels: [], body: "Fixes #1" }, + }, + }); + expect(seen.posted).toEqual([]); + }); + }); +}); diff --git a/test/unit/queue-lifecycle-guards.test.ts b/test/unit/queue-lifecycle-guards.test.ts new file mode 100644 index 0000000000..efca430c7a --- /dev/null +++ b/test/unit/queue-lifecycle-guards.test.ts @@ -0,0 +1,4839 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { generateKeyPairSync } from "node:crypto"; +import { clearInstallationTokenCacheForTest } from "../../src/github/app"; +import { clearReviewSuppressionCacheForTest } from "../../src/review/review-memory-wire"; +import { PR_PANEL_COMMENT_MARKER } from "../../src/github/comments"; +import * as backfillModule from "../../src/github/backfill"; +import * as rateLimitModule from "../../src/github/rate-limit"; +import * as repositoriesModule from "../../src/db/repositories"; +import * as reviewEffortModule from "../../src/review/review-effort"; +import * as repositorySettingsModule from "../../src/settings/repository-settings"; +import * as sentryModule from "../../src/selfhost/sentry"; +import { renderMetrics, resetMetrics } from "../../src/selfhost/metrics"; +import { jobCoalesceKey } from "../../src/selfhost/queue-common"; +import { + listCollisionEdges, + createAgentRun, + getCommandUsefulnessSummary, + getBurdenForecast, + getContributorEvidence, + getAgentRun, + getContributorScoringProfile, + getWebhookEvent, + getInstallation, + getLatestUpstreamRulesetSnapshot, + getPullRequest, + getPullRequestDetailSyncState, + upsertPullRequestDetailSyncState, + getRepository, + listUpstreamDriftReports, + listInstallationHealth, + listProductUsageDailyRollups, + listProductUsageEvents, + listPullRequests, + listPullRequestFiles, + listRepoSyncStates, + listSignalSnapshots, + persistSignalSnapshot, + recordGateBlockOutcome, + markGateOutcomeOverridden, + recordProductUsageEvent, + upsertAgentCommandAnswer, + upsertCheckSummary, + upsertIssueFromGitHub, + upsertRepoSyncSegment, + upsertInstallation, + updatePullRequestSlopAssessment, + upsertOfficialMinerDetection, + upsertPullRequestFile, + upsertPullRequestFromGitHub, + upsertIssueWatchSubscription, + upsertRepositoryAiKey, + upsertRepositorySettings, + upsertRepositoryFromGitHub, + putCachedAiReview, + markAiReviewPublished, + putCachedAiSlopAdvisory, + putCachedLinkedIssueSatisfaction, + recordReviewSuppression, + listReviewSuppressions, + setGlobalAgentFrozen, +} from "../../src/db/repositories"; +import { agentMaintenanceHeadMatchesGate, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors"; +import type { PullRequestRecord } from "../../src/types"; +import { aiReviewCacheInputFingerprint } from "../../src/review/ai-review-cache-input"; +import { fingerprint as reviewMemoryFingerprint } from "../../src/review/review-memory-match"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; +import * as focusManifestLoaderModule from "../../src/signals/focus-manifest-loader"; +import { normalizeRegistryPayload } from "../../src/registry/normalize"; +import { persistRegistrySnapshot } from "../../src/registry/sync"; +import { + classifyPullRequestFreshness, + fetchPullRequestFreshness, +} from "../../src/github/pr-freshness"; +import { createTestEnv } from "../helpers/d1"; +import { ISSUE_WAKE_MAX_PRS, MERGE_WAKE_MAX_PRS, SWEEP_MAX_PRS } from "../../src/settings/agent-sweep"; +import { AGENT_LABEL_PENDING_CLOSURE, DEFAULT_LINKED_ISSUE_HARD_RULES } from "../../src/review/linked-issue-hard-rules"; + +vi.mock("../../src/github/pr-freshness", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + fetchPullRequestFreshness: vi.fn(async (_env: Env, args: { expectedHeadSha?: string | null }) => ({ + status: "current" as const, + liveHeadSha: args.expectedHeadSha ?? null, + liveState: "open", + liveLabels: [] as string[], + })), + }; +}); + +// The re-gate sweep now FANS OUT the heavy re-review + marker stamp into per-PR `agent-regate-pr` jobs +// (#audit-sweep-fanout). A test asserting the re-review/stamp side effects must run the sweep AND drain the +// per-PR jobs it enqueues. Returns the captured agent-regate-pr jobs for assertions. +async function sweepAndDrainPerPr(env: Env, repoFullName: string): Promise { + const fanned: import("../../src/types").JobMessage[] = []; + const send = env.JOBS.send.bind(env.JOBS); + env.JOBS.send = (async (message: import("../../src/types").JobMessage, options?: QueueSendOptions) => { + if (message.type === "agent-regate-pr") fanned.push(message); + return send(message, options); + }) as typeof env.JOBS.send; + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName }); + env.JOBS.send = send; + for (const job of fanned) await processJob(env, job); + return fanned; +} + + +function completeSegment(repoFullName: string, segment: "labels" | "open_issues" | "open_pull_requests") { + return { + repoFullName, + segment, + status: "complete" as const, + sourceKind: "test" as const, + mode: "resume" as const, + fetchedCount: 1, + expectedCount: 1, + pageCount: 1, + completedAt: "2026-05-25T00:00:00.000Z", + warnings: [], + }; +} + +type CommandAnswerFixture = Parameters[1]; + +function commandAnswer(id: string, command: string, overrides: Partial = {}): CommandAnswerFixture { + return { + id, + repoFullName: "JSONbored/gittensory", + issueNumber: 77, + command, + requestCommentId: 7, + responseCommentId: 9001, + responseUrl: "https://github.com/JSONbored/gittensory/pull/77#issuecomment-9001", + actorKind: "maintainer" as const, + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + metadata: {}, + ...overrides, + }; +} + +function commandAnswerBody(answerId: string, command: string): string { + return [ + "", + ``, + `Command: \`@gittensory ${command}\``, + "Feedback is aggregate-only.", + ].join("\n"); +} + +function queueMinerSnapshot(login: string) { + return { + source: "gittensor_api" as const, + githubId: "123", + githubUsername: login, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + issueDiscoveryScore: 0, + issueTokenScore: 0, + issueCredibility: 1, + isIssueEligible: false, + issueEligibleRepoCount: 0, + alphaPerDay: 0, + taoPerDay: 0, + usdPerDay: 0, + totals: { + pullRequests: 3, + mergedPullRequests: 2, + openPullRequests: 1, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + }, + repositories: [], + pullRequests: [], + issueLabels: [], + }; +} + +function b64(value: string): string { + return Buffer.from(value, "utf8").toString("base64"); +} + +function withProductUsageInsertFailure(env: Env): Env { + const db = env.DB as unknown as { prepare(sql: string): unknown; batch(statements: unknown[]): Promise }; + return { + ...env, + DB: { + prepare(sql: string) { + if (sql.includes("product_usage_events")) throw new Error("product usage insert failed"); + return db.prepare.call(db, sql); + }, + batch(statements: unknown[]) { + return db.batch.call(db, statements); + }, + } as unknown as D1Database, + }; +} + +async function generatePrivateKeyPem(): Promise { + const key = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); + const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; +} + +describe("changedPathsForGuardrail", () => { + it("collects current + rename paths and skips empty entries", () => { + const files = [ + { path: "src/a.ts", previousFilename: null }, + { path: "src/b.ts", previousFilename: "src/old-b.ts" }, // a rename contributes both names + { path: "", previousFilename: "" }, // an empty path AND empty rename are both skipped (both guard branches false) + ] as unknown as Parameters[0]; + expect(changedPathsForGuardrail(files)).toEqual(["src/a.ts", "src/b.ts", "src/old-b.ts"]); + }); +}); + +describe("agentMaintenanceHeadMatchesGate", () => { + it("allows maintenance only when the stored PR head still matches the reviewed gate head", () => { + expect(agentMaintenanceHeadMatchesGate("reviewed", "reviewed")).toBe(true); + expect(agentMaintenanceHeadMatchesGate("reviewed", "new-unreviewed")).toBe(false); + }); + + it("keeps legacy no-SHA paths fail-open because no exact reviewed head can be pinned", () => { + expect(agentMaintenanceHeadMatchesGate(undefined, "current")).toBe(true); + expect(agentMaintenanceHeadMatchesGate("reviewed", null)).toBe(true); + }); + + it("REGRESSION (#stale-head): a newer synchronize that advances the stored head before maintenance acts blocks the stale-gate auto-merge", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { + action: "created", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + target_type: "User", + repository_selection: "all", + permissions: { metadata: "read", contents: "write", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + // Clean, mergeable, approved, green CI + merge:auto + approve:auto + close:auto — this PR WOULD be auto-acted. + // The ONLY thing that must stop it is the stale-head guard in maybeRunAgentMaintenance. + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { merge: "auto", approve: "auto", close: "auto" }, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/stale1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/stale1/status")) return Response.json({ statuses: [] }); + if (url.includes("/check-runs")) return Response.json({ id: 902 }, { status: 201 }); + return new Response("not found", { status: 404 }); + }); + + // Simulate the concurrent-queue race the guard defends against: the gate evaluated head "stale1", but by the + // time maintenance re-reads the persisted row a newer `synchronize` has advanced the stored head to "newer2". + // maybeRunAgentMaintenance re-reads via getPullRequest, so divert that read to the advanced head. + const realGetPullRequest = repositoriesModule.getPullRequest; + const spy = vi.spyOn(repositoriesModule, "getPullRequest").mockImplementation(async (...callArgs) => { + const row = await realGetPullRequest(...callArgs); + return row ? { ...row, headSha: "newer2" } : row; + }); + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "stale-head-no-maintenance", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 71, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "stale1" }, labels: [], body: "Closes #1", mergeable_state: "clean", reviewDecision: "APPROVED" }, + }, + }); + } finally { + spy.mockRestore(); + } + + // No terminal maintenance action of ANY class fires: the gate verdict belonged to the now-stale head. + const acted = await env.DB.prepare("select count(*) as n from audit_events where event_type in ('agent.action.merge','agent.action.approve','agent.action.close')").first<{ n: number }>(); + expect(acted?.n).toBe(0); + }); +}); + +describe("one-shot reopen prevention", () => { + beforeEach(() => { + clearInstallationTokenCacheForTest(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("re-closes contributor reopens after a write collaborator closed the PR", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + // Both a "closed" event (by the write collaborator) AND a "reopened" event (by the contributor, still the + // most recent reopener) — the new live re-check (#2369) reads this same endpoint to confirm the contributor + // is still the current reopener before proceeding to close. + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); // opted into acting autonomy + + await processJob(env, { + type: "github-webhook", + deliveryId: "reopen-write-collab-close", + eventName: "pull_request", + payload: reopenedPayload("contributor"), + }); + + expect(calls.some((call) => call.url.endsWith("/collaborators/contributor/permission"))).toBe(true); + expect(calls.some((call) => call.url.endsWith("/collaborators/maintainer/permission"))).toBe(true); + expect(calls.some((call) => call.method === "POST" && call.url.endsWith("/issues/42/comments"))).toBe(true); + expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(true); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); // #2260: a successful close is unaffected + expect(audit?.detail).toContain("originally closed by maintainer"); + // #review-audit: the early return after a re-close stamps the delivery processed (was left "queued"). + const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("reopen-write-collab-close").first<{ status: string }>(); + expect(webhookRow?.status).toBe("processed"); + }); + + it("does NOT re-close a disallowed reopen when live PR state has moved since the webhook was received (#2130, #2261)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + // A maintainer legitimately reopened/re-approved the PR — or a queue retry replayed a stale payload — in + // the window between the original webhook delivery and this handler's permission/closer-history reads. The + // live re-check must catch it and deny the re-close rather than overwriting a live maintainer decision. + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "head_changed", expectedHeadSha: "abc123", liveHeadSha: "def456", liveState: "open" }); + + await processJob(env, { type: "github-webhook", deliveryId: "reopen-stale", eventName: "pull_request", payload: reopenedPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("reopen re-close not executed"); + }); + + it("REGRESSION: does NOT re-close when the reopener gained maintainer permission before the close fires (#2130 follow-up)", async () => { + // Same head, still open — a head/state-only freshness check would say "current". But the reopener could + // have been promoted to a write/maintain/admin collaborator (or added as one) in the window between the + // initial permission read and this handler's close, which retroactively authorizes exactly the reopen + // this handler is about to undo. + const calls: Array<{ url: string; method: string }> = []; + let contributorPermissionCalls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) { + contributorPermissionCalls += 1; + // First read (upstream decision to re-close at all): still just a reader. Second read (the live + // re-check right before the mutation): promoted to a write collaborator. + return Response.json({ permission: contributorPermissionCalls === 1 ? "read" : "write" }); + } + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + + await processJob(env, { type: "github-webhook", deliveryId: "reopen-promoted", eventName: "pull_request", payload: reopenedPayload("contributor") }); + + expect(contributorPermissionCalls).toBe(2); + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("now holds maintainer permission"); + }); + + it("REGRESSION: does NOT re-close when a DIFFERENT (maintainer) reopener supersedes the original disallowed reopen (#2369)", async () => { + // The original contributor reopen is what triggered this handler, but by the time it runs, a real maintainer + // has ALSO reopened the same PR (a legitimate, authorized reopen is now the current reason it's open). Head/ + // state freshness and the reopener's OWN permission re-check both miss this — neither sees WHO most recently + // reopened. The timeline shows a "closed" event by "maintainer" (the original one-shot close) followed by a + // LATER "reopened" event by a different maintainer login ("second-maintainer"), after the contributor's own + // earlier reopen. + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) { + return Response.json([ + { event: "closed", actor: { login: "maintainer" } }, + { event: "reopened", actor: { login: "contributor" } }, + { event: "closed", actor: { login: "maintainer" } }, + { event: "reopened", actor: { login: "second-maintainer" } }, + ]); + } + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + + await processJob(env, { type: "github-webhook", deliveryId: "reopen-superseded", eventName: "pull_request", payload: reopenedPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("second-maintainer"); + expect(audit?.detail).toContain("not contributor"); + }); + + it("happy path unaffected: re-closes when the same reopener is still the latest reopener on the timeline (#2369)", async () => { + // Confirms the new live re-check does not spuriously block the ordinary case: the contributor is BOTH the + // original AND the still-current reopener (no one else reopened it again in the meantime). + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + + await processJob(env, { type: "github-webhook", deliveryId: "reopen-same-latest", eventName: "pull_request", payload: reopenedPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + }); + + it("REGRESSION: denies when padding makes the latest reopener ambiguous beyond the inspected event window", async () => { + const calls: Array<{ url: string; method: string }> = []; + const eventPages: number[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.includes("/issues/42/events")) { + const page = Number(new URL(url).searchParams.get("page") ?? "1"); + eventPages.push(page); + if (page === 1) { + return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }], { + headers: { link: '; rel="last"' }, + }); + } + if (page === 12) return Response.json([{ event: "reopened", actor: { login: "second-maintainer" } }]); + return Response.json([{ event: "renamed", actor: { login: "contributor" } }]); + } + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + + await processJob(env, { type: "github-webhook", deliveryId: "reopen-window-stuffed", eventName: "pull_request", payload: reopenedPayload("contributor") }); + + expect(eventPages).toContain(22); + expect(eventPages).not.toContain(12); + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("the current reopener is now unknown, not contributor"); + }); + + it("REGRESSION: fails CLOSED (denies the re-close) when the reopener-timeline read errors (#2369)", async () => { + // The reopener-timeline lookup errors (network failure) → getLastReopenerLogin catches and returns + // { login: null, coveredAllPages: false, errored: true } — DISTINCT from the padded-window case above + // (which has errored: false). The design explicitly fails CLOSED here (deny the close) rather than + // proceeding, since wrongly re-closing a maintainer-authorized PR is worse than leaving a disallowed + // reopen open for one more tick. + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) throw new Error("GitHub events API down"); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + + await processJob(env, { type: "github-webhook", deliveryId: "reopen-timeline-error", eventName: "pull_request", payload: reopenedPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("could not confirm"); + }); + + it("REGRESSION: denies with an 'unknown' current-reopener detail when the timeline genuinely has no reopen event at all (#2369)", async () => { + // The window is FULLY covered (a single page, no Link header) but contains no "reopened" event whatsoever — + // getLastReopenerLogin returns { login: null, coveredAllPages: true }, which is NOT the ambiguous case (that + // requires coveredAllPages: false); it lands on the "superseded by a different actor" arm with a null login, + // exercising the `latestReopenerLogin ?? "unknown"` fallback in the audit detail. + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + + await processJob(env, { type: "github-webhook", deliveryId: "reopen-no-reopen-event", eventName: "pull_request", payload: reopenedPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("the current reopener is now unknown, not contributor"); + }); + + it("swallows a recordAuditEvent failure on the superseded-reopener denial path — handler still completes (#2369)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) { + return Response.json([ + { event: "closed", actor: { login: "maintainer" } }, + { event: "reopened", actor: { login: "contributor" } }, + { event: "closed", actor: { login: "maintainer" } }, + { event: "reopened", actor: { login: "second-maintainer" } }, + ]); + } + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "reopen-superseded-audit-fail", eventName: "pull_request", payload: reopenedPayload("contributor") }), + ).resolves.toBeUndefined(); + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("swallows a recordAuditEvent failure on the stale-reopen denial path — handler still completes (#2130)", async () => { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "head_changed", expectedHeadSha: "abc123", liveHeadSha: "def456", liveState: "open" }); + vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "reopen-stale-audit-fail", eventName: "pull_request", payload: reopenedPayload("contributor") }), + ).resolves.toBeUndefined(); + }); + + it("swallows a recordAuditEvent failure on the promoted-reopener denial path — handler still completes (#2130 follow-up)", async () => { + let contributorPermissionCalls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) { + contributorPermissionCalls += 1; + return Response.json({ permission: contributorPermissionCalls === 1 ? "read" : "write" }); + } + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "reopen-promoted-audit-fail", eventName: "pull_request", payload: reopenedPayload("contributor") }), + ).resolves.toBeUndefined(); + expect(contributorPermissionCalls).toBe(2); + }); + + it("records outcome:error (not completed) when the reclose PATCH call itself fails (#2260)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + // "contributor" (the payload's reopener) must be the MOST RECENT "reopened" actor in the timeline, or the + // #2369 live-recheck #3 (reopenerSuperseded) denies before ever reaching the close attempt this test targets. + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); // the courtesy comment succeeds + if (url.endsWith("/pulls/42") && method === "PATCH") return new Response("forbidden", { status: 403 }); // the close itself fails + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "reopen-close-fails", + eventName: "pull_request", + payload: reopenedPayload("contributor"), + }); + + expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(true); // the close WAS attempted + const audit = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string; metadata_json: string }>(); + expect(audit?.outcome).toBe("error"); // NOT "completed" — the close did not actually succeed + expect(audit?.detail).toContain("FAILED to re-close"); + expect(JSON.parse(audit?.metadata_json ?? "{}").error).toBeTruthy(); + // The handler still owns the decision (never falls through to normal re-review) even though the API call failed. + const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("reopen-close-fails").first<{ status: string }>(); + expect(webhookRow?.status).toBe("processed"); + }); + + it("retries the reopen-reclose when a concurrent delivery already holds the per-PR actuation lock (#2447)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto" } }); + // Simulates a DIFFERENT concurrent delivery for the same PR already in flight (e.g. the draft-dodge sibling + // racing this reopen) — the lock key it would hold is pre-claimed here. + await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:jsonbored/gittensory#42", "1", 60); + // A contended lock must still stop before resolveRepositorySettings, the first call the normal re-review makes, + // but must NOT stamp this reopen delivery processed: the lock holder may be an unrelated same-PR guard that + // no-ops, so the queue needs to retry this reopen guard once the lock clears. + const resolveSettingsSpy = vi.spyOn(repositorySettingsModule, "resolveRepositorySettings"); + + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "reopen-lock-contended", + eventName: "pull_request", + payload: reopenedPayload("contributor"), + }), + ).rejects.toMatchObject({ retryKind: "pr_actuation_lock_contended" }); + + expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ n: number }>(); + expect(audit?.n).toBe(0); // no decision recorded either way — retry owns the eventual reopen decision + expect(resolveSettingsSpy).not.toHaveBeenCalled(); // the normal re-review pass never started + const webhookRow = await env.DB.prepare("select status, error_summary from webhook_events where delivery_id = ?").bind("reopen-lock-contended").first<{ status: string; error_summary: string }>(); + expect(webhookRow?.status).toBe("error"); + expect(webhookRow?.error_summary).toContain("pr actuation lock contended"); + }); + + it("does NOT re-close a disallowed reopen on an OBSERVE-only / un-opted-in repo (autonomy floor, #review-audit)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + // NO autonomy configured (observe-only / un-opted-in): the agent must take NO destructive action. + await processJob(env, { type: "github-webhook", deliveryId: "reopen-observe-only", eventName: "pull_request", payload: reopenedPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); // never commented + }); + + it("does NOT re-close a disallowed reopen while the global freeze is on — records a skip instead (#killswitch-gap)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); // opted into acting autonomy + await repositoriesModule.setGlobalAgentFrozen(env, true); // emergency brake on + await processJob(env, { type: "github-webhook", deliveryId: "reopen-frozen", eventName: "pull_request", payload: reopenedPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("skipped (agent paused)"); + }); + + it("dry-run: audits a would-be reopen re-close without touching GitHub (#killswitch-gap)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true, autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); + await processJob(env, { type: "github-webhook", deliveryId: "reopen-dryrun", eventName: "pull_request", payload: reopenedPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toContain("dry-run: would re-close"); + }); + + it("allows an admin reopener to reopen without reclosing (fast-path hasMaintainerPermission)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push({ url, method: init?.method ?? "GET" }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory", ADMIN_GITHUB_LOGINS: "admin-user" }); + await processJob(env, { type: "github-webhook", deliveryId: "admin-reopen", eventName: "pull_request", payload: reopenedPayload("admin-user") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("allows reopen when the closer is unknown (null lastCloser)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push({ url, method: init?.method ?? "GET" }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.includes("/issues/42/events")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await processJob(env, { type: "github-webhook", deliveryId: "unknown-closer", eventName: "pull_request", payload: reopenedPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("re-closes when the close event is hidden beyond the inspected event window (window-evasion fail-closed, #audit-2.4)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.includes("/issues/42/events")) { + // Long timeline (lastPage=12): the contributor padded the events so the real close sits before the + // inspected newest window. No "closed" appears in the read pages → null closer + coveredAllPages=false. + // The tail DOES include the contributor's own "reopened" event (the one this whole handler is reacting + // to), so the new live re-check (#2369) still finds `contributor` as the current reopener and does not + // itself block the re-close — only the (deliberately fail-closed) window-evasion path above does. + const page = Number(new URL(url).searchParams.get("page") ?? "1"); + if (page === 1) { + return Response.json([{ event: "labeled", actor: { login: "contributor" } }], { + headers: { link: '; rel="last"' }, + }); + } + if (page === 12) return Response.json([{ event: "labeled", actor: { login: "contributor" } }, { event: "reopened", actor: { login: "contributor" } }]); + return Response.json([{ event: "labeled", actor: { login: "contributor" } }]); + } + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); // opted into acting autonomy + await processJob(env, { type: "github-webhook", deliveryId: "window-evasion-reclose", eventName: "pull_request", payload: reopenedPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ detail: string }>(); + expect(audit?.detail).toContain("beyond the inspected event window"); + }); + + it("re-closes when the bot itself was the last closer", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "gittensory[bot]" } }, { event: "reopened", actor: { login: "contributor" } }]); + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); // opted into acting autonomy + await processJob(env, { type: "github-webhook", deliveryId: "bot-closer-reclose", eventName: "pull_request", payload: reopenedPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + }); + + it("allows reopen when a contributor self-closed (non-maintainer, non-bot closer)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push({ url, method: init?.method ?? "GET" }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "contributor" } }]); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await processJob(env, { type: "github-webhook", deliveryId: "self-close-reopen", eventName: "pull_request", payload: reopenedPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("treats permission API errors as non-maintainer (catch path returns null)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push({ url, method: init?.method ?? "GET" }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/") && url.endsWith("/permission")) throw new Error("permission API down"); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "contributor" } }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await processJob(env, { type: "github-webhook", deliveryId: "perm-api-error", eventName: "pull_request", payload: reopenedPayload("contributor") }); + // permission API threw → null → non-maintainer reopener + non-maintainer closer → no reclose. + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("swallows createIssueComment, closePullRequest, and recordAuditEvent errors on reclose (fail-safe — all .catch() bodies)", async () => { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + // createIssueComment (POST) and closePullRequest (PATCH) both throw → their .catch(() => undefined) bodies run + throw new Error("GitHub API unavailable"); + }); + vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await expect( + processJob(env, { type: "github-webhook", deliveryId: "reopen-api-fail-safe", eventName: "pull_request", payload: reopenedPayload("contributor") }), + ).resolves.toBeUndefined(); + }); + + it("REGRESSION (#4602): does NOT re-close a disallowed reopen when close autonomy is unconfigured, even though another class (merge) is auto", async () => { + // Before #4602, this guard gated only on isAgentConfigured(autonomy) -- true here because `merge` is + // acting -- with no check on the `close` action class specifically. A repo that opts into merge/review + // autonomy but deliberately leaves close unconfigured (deny-by-default) must NOT have PRs re-closed here. + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto" } }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "reopen-close-autonomy-unconfigured", + eventName: "pull_request", + payload: reopenedPayload("contributor"), + }); + + expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(false); + expect(calls.some((call) => call.method === "POST" && call.url.endsWith("/issues/42/comments"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("autonomy for close is not acting"); + expect(audit?.detail).toContain("reopen re-close not enforced for contributor"); + }); + + it("REGRESSION (#4602): denies with an approval-required message when close autonomy is auto_with_approval", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto_with_approval" } }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "reopen-close-autonomy-approval", + eventName: "pull_request", + payload: reopenedPayload("contributor"), + }); + + expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("close autonomy requires approval"); + }); +}); + +describe("converted_to_draft gate-close (draft-dodge prevention)", () => { + beforeEach(() => clearInstallationTokenCacheForTest()); + afterEach(() => { + clearInstallationTokenCacheForTest(); + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + function draftPayload(author: string, headSha = "abc123", isDraft = true): any { + return { + action: "converted_to_draft", + installation: { id: 123 }, + repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, + sender: { login: author, type: "User" }, + pull_request: { + id: 4242, + number: 42, + state: "open", + title: "Some PR", + body: "Body.", + user: { login: author }, + head: { sha: headSha, ref: "fix", repo: { full_name: `${author}/gittensory`, owner: { login: author } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: isDraft, + merged: false, + mergeable_state: "clean", + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + }, + }; + } + + async function setupRepo(env: ReturnType, overrides: Record = {}): Promise { + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto" }, + agentPaused: false, + ...overrides, + }); + } + + it("closes a PR immediately when the contributor converts to draft after a gate failure on the same headSha", async () => { + const calls: Array<{ url: string; method: string; body?: unknown }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method, body: init?.body ? JSON.parse(String(init.body)) : undefined }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-1", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ detail: string }>(); + expect(audit?.detail).toContain("abc123"); + expect(audit?.detail).toContain("contributor"); + }); + + it("does NOT draft-dodge close when live PR state has moved since the webhook was received (#2130)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + // A maintainer merged/closed the PR — or a fresh commit resolved the gate failure — in the window between + // webhook ingestion and this handler's async DB reads (getGateBlockOutcome, isGlobalAgentFrozen). The live + // re-check must catch it and deny the close rather than firing blind off the stale ingestion-time payload. + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "closed", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "closed" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-stale", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("draft-dodge close not executed"); + }); + + it("REGRESSION: does NOT draft-dodge close when the PR was converted back to ready_for_review before the close fires (#2130 follow-up)", async () => { + // Same head, still open — a head/state-only freshness check would say "current". But the draft-dodge + // close's whole justification is "the author is dodging the gate via draft state", which no longer holds + // once the PR is ready_for_review again — closing here would be wrong even though nothing else moved. + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "no_longer_draft", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "open" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-no-longer-draft", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ requireDraft: true })); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("no longer a draft"); + }); + + it("swallows a recordAuditEvent failure on the stale-draft-dodge denial path — handler still completes (#2130)", async () => { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "closed", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "closed" }); + vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-stale-audit-fail", eventName: "pull_request", payload: draftPayload("contributor") }), + ).resolves.toBeUndefined(); + }); + + it("denies the draft-dodge close (never attempts it) when pull_requests: write is not granted (#2134)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + // Installation grant is missing pull_requests: write (revoked or never consented) — issues: write is present, + // so this isn't a blanket permission failure, just the specific scope this close needs. + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "enabled", reviewCheckMode: "required", autonomy: { close: "auto" }, agentPaused: false }); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-no-write", eventName: "pull_request", payload: draftPayload("contributor") }); + + // Neither the close nor its accompanying comment was attempted — a 403 from GitHub is never reached. + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("pull_requests: write not granted"); + }); + + it("denies the draft-dodge close when no installation row was pre-synced and the webhook payload carries no permissions", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + // No installations row pre-seeded. processGitHubWebhook auto-upserts one from the payload's bare + // `installation: { id: 123 }` (no permissions field, as a real pull_request payload carries), so the + // resulting row has no explicit pull_requests:write grant — the permission check must fail CLOSED (deny). + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "enabled", reviewCheckMode: "required", autonomy: { close: "auto" }, agentPaused: false }); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-no-install-row", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("denied"); + }); + + it("REGRESSION: a transient getInstallation read failure during the draft-dodge readiness check propagates (retries) instead of misrecording a permission denial", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "enabled", reviewCheckMode: "required", autonomy: { close: "auto" }, agentPaused: false }); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + // First getInstallation call in processGitHubWebhook (installationActor derivation, unrelated to this fix) + // resolves normally; the SECOND call is the draft-dodge readiness check itself -- that one is a genuine D1 + // read failure, not a "row not found." + const getInstallationSpy = vi.spyOn(repositoriesModule, "getInstallation"); + getInstallationSpy.mockResolvedValueOnce({ + id: 123, + accountLogin: "JSONbored", + accountId: 1, + appId: null, + targetType: "User", + repositorySelection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + suspendedAt: null, + createdAt: null, + updatedAt: null, + }); + getInstallationSpy.mockRejectedValueOnce(new Error("D1 read failed")); + + await expect(processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-install-read-fails", eventName: "pull_request", payload: draftPayload("contributor") })).rejects.toThrow("D1 read failed"); + + // Neither the close nor its accompanying comment was attempted -- the failure short-circuits before either. + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + // No misleading "pull_requests: write not granted" audit -- the webhook's own top-level catch records the + // actual error instead, which the queue's standard retry-on-throw semantics will re-attempt. + const draftDodgeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ n: number }>(); + expect(draftDodgeAudit?.n).toBe(0); + const webhookAudit = await env.DB.prepare("select status, error_summary from webhook_events where delivery_id = ?").bind("draft-dodge-install-read-fails").first<{ status: string; error_summary: string | null }>(); + expect(webhookAudit?.status).toBe("error"); + expect(webhookAudit?.error_summary).toContain("D1 read failed"); + }); + + it("does NOT draft-dodge close while the global freeze is on (#killswitch-gap)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + await repositoriesModule.setGlobalAgentFrozen(env, true); + await processJob(env, { type: "github-webhook", deliveryId: "draft-frozen", eventName: "pull_request", payload: draftPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed under freeze + expect(await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ n: number }>()).toMatchObject({ n: 0 }); + }); + + it("dry-run: audits a would-be draft-dodge close without touching GitHub (#killswitch-gap)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env, { agentDryRun: true }); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-dryrun", eventName: "pull_request", payload: draftPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed in dry-run + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toContain("dry-run: would close"); + }); + + it("retries the draft-dodge close when a concurrent delivery already holds the per-PR actuation lock (#2447)", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + // Simulates a DIFFERENT concurrent delivery for the same PR already in flight (e.g. a check_suite completion + // racing this converted_to_draft event) — the lock key it would hold is pre-claimed here. + await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:jsonbored/gittensory#42", "1", 60); + + await expect(processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-lock-contended", eventName: "pull_request", payload: draftPayload("contributor") })).rejects.toThrow("pr actuation lock contended"); + + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ n: number }>(); + expect(audit?.n).toBe(0); // no decision recorded either way — the queue retry owns the deferred decision + }); + + it("REGRESSION: exactly ONE of two genuinely concurrent draft-dodge deliveries for the SAME PR wins the actuation lock (#2135)", async () => { + // Unlike the lock-contended test above (which pre-seeds the key before the call even starts), this fires + // two deliveries together via Promise.all with NEITHER pre-claiming anything — exercising the actual + // check-and-set race claimPrActuationLock must arbitrate, not just "the key was already there". A + // get-then-set (non-atomic) implementation lets both deliveries observe an absent key and both proceed, + // which this test would catch as more than one PATCH / more than one completed audit row. + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + const results = await Promise.allSettled([ + processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-race-a", eventName: "pull_request", payload: draftPayload("contributor") }), + processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-race-b", eventName: "pull_request", payload: draftPayload("contributor") }), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + expect(results.filter((result) => result.status === "rejected")).toHaveLength(1); + + const patchCalls = calls.filter((c) => c.includes("PATCH") && c.includes("/pulls/42")); + expect(patchCalls).toHaveLength(1); // exactly one delivery won the race and closed the PR + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and outcome = 'completed'").bind("github_app.draft_dodge_closed").first<{ n: number }>(); + expect(audit?.n).toBe(1); // exactly one completed close recorded — not two (the race), not zero + }); + + it("no-ops when no prior gate failure exists for the PR", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + // No gate block recorded — gate hasn't run yet. + + await processJob(env, { type: "github-webhook", deliveryId: "draft-no-block", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + }); + + it("no-ops when the prior gate failure is for a different headSha (contributor pushed fixes in draft)", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + // Block exists but for an OLDER commit — contributor has pushed new code in draft. + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "old-sha-XYZ", blockerCodes: ["missing_linked_issue"] }); + + // Payload headSha is "abc123" (new commit), not "old-sha-XYZ". + await processJob(env, { type: "github-webhook", deliveryId: "draft-new-sha", eventName: "pull_request", payload: draftPayload("contributor", "abc123") }); + + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + }); + + it("no-ops when the gate block has been maintainer-overridden", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + await markGateOutcomeOverridden(env, "JSONbored/gittensory", 42); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-overridden", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + }); + + it("no-ops when the PR author is the repo owner (owner PRs are never auto-closed)", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + // Author = "JSONbored" = repo owner → no close. + await processJob(env, { type: "github-webhook", deliveryId: "draft-owner", eventName: "pull_request", payload: draftPayload("JSONbored") }); + + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + }); + + it("no-ops when the PR author is an ADMIN_GITHUB_LOGINS fleet-operator, not just the literal repo owner (#2133)", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory", ADMIN_GITHUB_LOGINS: "admin-user" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + // Author = "admin-user" ≠ repo owner "JSONbored", but IS in ADMIN_GITHUB_LOGINS → no close. + await processJob(env, { type: "github-webhook", deliveryId: "draft-admin", eventName: "pull_request", payload: draftPayload("admin-user") }); + + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + }); + + it("no-ops when the agent is paused (agentPaused=true)", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env, { agentPaused: true }); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-paused", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + }); + + it("no-ops when the agent autonomy is not configured (autonomy=null)", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env, { autonomy: null }); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-no-autonomy", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + }); + + it("closes with empty blockerCodes (no codes parenthetical) and null author (uses 'unknown' in audit)", async () => { + // covers: codes ? `(${codes})` : "" → "" branch; pr.authorLogin ?? "unknown" → "unknown" branch + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push({ url, method: init?.method ?? "GET" }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42") && (init?.method ?? "GET") === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + // empty blockerCodes → codes = "" → ternary takes the "" branch + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: [] }); + + // null user.login → authorLogin null → (null ?? "").toLowerCase() === "" ≠ "jsonbored" → authorIsOwner false → close proceeds + // → pr.authorLogin ?? "unknown" in audit detail takes the "unknown" branch + const payload = draftPayload("contributor"); + payload.pull_request.user = { login: null }; + await processJob(env, { type: "github-webhook", deliveryId: "empty-codes-null-author", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + const comment = calls.find((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments")); + expect(comment).toBeDefined(); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ detail: string }>(); + expect(audit?.detail).toContain("unknown"); + }); + + it("swallows createIssueComment and closePullRequest API errors (fail-safe — both .catch() bodies)", async () => { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + throw new Error("simulated network error"); // all GitHub calls throw + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + // Should not throw even though createIssueComment and closePullRequest both throw + await expect( + processJob(env, { type: "github-webhook", deliveryId: "api-error-swallow", eventName: "pull_request", payload: draftPayload("contributor") }), + ).resolves.toBeUndefined(); + + // Audit event was still written to DB (recordAuditEvent uses D1, not fetch) + const audit = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ event_type: string }>(); + expect(audit?.event_type).toBe("github_app.draft_dodge_closed"); + }); + + it("getGateBlockOutcome DB error is caught — handler no-ops gracefully", async () => { + const calls: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + calls.push(`${init?.method ?? "GET"} ${input}`); + if (input.toString().includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + // Spy on getGateBlockOutcome to throw — the .catch(() => undefined) body must execute + vi.spyOn(repositoriesModule, "getGateBlockOutcome").mockRejectedValueOnce(new Error("D1 error")); + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "gbo-db-error", eventName: "pull_request", payload: draftPayload("contributor") }), + ).resolves.toBeUndefined(); + + // No close should have happened (block was unknown due to DB error) + expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); + }); + + it("recordAuditEvent failure is swallowed — close still proceeds without crashing", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push({ url, method: init?.method ?? "GET" }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42") && (init?.method ?? "GET") === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "audit-db-error", eventName: "pull_request", payload: draftPayload("contributor") }), + ).resolves.toBeUndefined(); + + // Close still happened despite audit failure + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + }); + + it("no-op owner-exemption when repoFullName has no slash (repoOwner is empty — authorIsOwner always false)", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push({ url, method: init?.method ?? "GET" }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/issues/") && (init?.method ?? "GET") === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/pulls/") && (init?.method ?? "GET") === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + // Setup with a slash-free repo name + await upsertRepositoryFromGitHub(env, { name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }, 200); + await upsertInstallation(env, { + installation: { + id: 200, + account: { login: "", id: 2, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "noslash", + gateCheckMode: "enabled", reviewCheckMode: "required", + autonomy: { close: "auto" }, + agentPaused: false, + }); + await recordGateBlockOutcome(env, { repoFullName: "noslash", pullNumber: 77, headSha: "sha-noslash", blockerCodes: ["missing_linked_issue"] }); + + const noslashPayload = { + action: "converted_to_draft", + installation: { id: 200 }, + repository: { id: 2, name: "noslash", full_name: "noslash", private: false, default_branch: "main", owner: { login: "" } }, + sender: { login: "someone", type: "User" }, + pull_request: { + id: 9999, + number: 77, + state: "open", + title: "slash-free", + body: "", + user: { login: "someone" }, + head: { sha: "sha-noslash", ref: "fix", repo: { full_name: "someone/noslash", owner: { login: "someone" } } }, + base: { sha: "base", ref: "main", repo: { full_name: "noslash", owner: { login: "" } } }, + draft: true, + merged: false, + mergeable_state: "clean", + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + }, + }; + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "noslash-test", eventName: "pull_request", payload: noslashPayload }), + ).resolves.toBeUndefined(); + + // With no slash in repoFullName: repoOwner="" (branch 196 false) → repoOwner.length>0=false (branch 198 false) + // → authorIsOwner=false → handler enters the close path. closePullRequest/.catch() swallows the splitRepo + // error (GitHub API requires owner/repo — slash-free names can't be closed via API) but the handler itself + // doesn't crash. Verify the handler DID reach getGateBlockOutcome, proving branches 196+198 were exercised. + const verifyBlock = await repositoriesModule.getGateBlockOutcome(env, "noslash", 77); + expect(verifyBlock?.headSha).toBe("sha-noslash"); + }); + + it("REGRESSION (#4602): does NOT draft-dodge close when close autonomy is unconfigured, even though another PR-write class (approve) is auto and pull_requests:write IS granted", async () => { + // Before #4602, resolveAgentPermissionReadiness's missing actionClass:"close" checked the UNION of every + // acting class's write-permission grant, not close's specifically -- `approve` is a PR-write class and + // pull_requests:write IS granted here (setupRepo's default), so readiness alone used to read "ready" and + // let the close proceed despite close itself never being authorized. + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); + if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env, { autonomy: { approve: "auto" } }); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-close-autonomy-unconfigured", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("autonomy for close is not acting"); + expect(audit?.detail).toContain("draft-dodge close not enforced for contributor"); + }); + + it("REGRESSION (#4602): denies with an approval-required message when close autonomy is auto_with_approval", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push({ url, method: init?.method ?? "GET" }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupRepo(env, { autonomy: { close: "auto_with_approval" } }); + await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-close-autonomy-approval", eventName: "pull_request", payload: draftPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("close autonomy requires approval"); + }); +}); + +function draftEvasionPayload(author: string, headSha = "abc123"): any { + return { + action: "converted_to_draft", + installation: { id: 123 }, + repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, + sender: { login: author, type: "User" }, + pull_request: { + id: 4242, + number: 42, + state: "open", + title: "Some PR", + body: "Body.", + user: { login: author }, + head: { sha: headSha, ref: "fix", repo: { full_name: `${author}/gittensory`, owner: { login: author } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: true, + merged: false, + mergeable_state: "clean", + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + }, + }; +} + +function closedPayload(sender: string, author = sender, headSha = "abc123"): any { + return { + action: "closed", + installation: { id: 123 }, + repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, + sender: { login: sender, type: "User" }, + pull_request: { + id: 4242, + number: 42, + state: "closed", + title: "Some PR", + body: "Body.", + user: { login: author }, + head: { sha: headSha, ref: "fix", repo: { full_name: `${author}/gittensory`, owner: { login: author } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: false, + merged: false, + mergeable_state: "clean", + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + }, + }; +} + +describe("review-evasion protection (#review-evasion-protection)", () => { + beforeEach(() => clearInstallationTokenCacheForTest()); + afterEach(() => { + clearInstallationTokenCacheForTest(); + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + async function setupEvasionRepo(env: ReturnType, overrides: Record = {}): Promise { + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + publicSurface: "off", + commentMode: "off", + checkRunMode: "off", + autonomy: { close: "auto" }, + agentPaused: false, + reviewEvasionProtection: "close", + ...overrides, + }); + } + + // Generic GitHub fetch stub covering every endpoint the evasion handlers (and the surrounding webhook + // pipeline they run inside) can call. `collaboratorPermission` controls what a non-owner/non-admin closer's + // permission check reports (default "read" — an ordinary contributor). + function stubEvasionFetch(calls: Array<{ url: string; method: string }>, opts: { collaboratorPermission?: string; onPatch?: (url: string) => Response | null } = {}) { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: opts.collaboratorPermission ?? "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) { + const custom = opts.onPatch?.(url); + if (custom) return custom; + return Response.json({ state: url === "open" ? "open" : "closed" }); + } + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); + if (url.includes("/labels")) return Response.json([{ name: "review-evasion" }]); + if (url.includes("/pulls/42/files")) return Response.json([]); + // A .gittensory.yml content fetch (raw.githubusercontent.com) must resolve to SOMETHING with no opinion + // on reviewEvasionProtection -- otherwise a miss here falls through to the bundled JSONbored/gittensory + // fallback manifest (gittensory-repo-focus-manifest.ts), whose OWN checked-in reviewEvasionProtection: + // close would silently outrank every test below's DB-level override (yml > DB precedence, #config-as-code). + if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); + return new Response("not found", { status: 404 }); + }); + } + + describe("self-close during an active review", () => { + it("reopens then re-closes as the App, posts the explanation comment, applies the label, and records a review_evasion strike", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", authorLogin: "contributor", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-1", eventName: "pull_request", payload: closedPayload("contributor") }); + + const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); + expect(patches.length).toBeGreaterThanOrEqual(2); // reopen (state=open) then re-close (state=closed) + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toContain("contributor"); + expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(false); // terminalized + const strike = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ outcome: string }>(); + expect(strike?.outcome).toBe("completed"); + }); + + it("reopens and re-closes when the live self-closed PR is already closed on the reviewed head", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", authorLogin: "contributor", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ + status: "stale", + reason: "closed", + expectedHeadSha: "abc123", + liveHeadSha: "ABC123", + liveState: "closed", + }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-live-closed", eventName: "pull_request", payload: closedPayload("contributor") }); + + const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); + expect(patches.length).toBeGreaterThanOrEqual(2); // same-head closed is the normal self-close state: reopen then re-close + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(false); + const strike = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ outcome: string }>(); + expect(strike?.outcome).toBe("completed"); + }); + + it("retries (via a thrown lock-contended error) when a concurrent delivery already holds the per-PR actuation lock", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:jsonbored/gittensory#42", "1", 60); + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "self-close-lock-contended", eventName: "pull_request", payload: closedPayload("contributor") }), + ).rejects.toThrow("during review-evasion-self-close"); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ n: number }>(); + expect(audit?.n).toBe(0); // no decision recorded either way -- the queue retry owns the deferred decision + }); + + it("does nothing when reviewEvasionProtection is explicitly off (#4011: the only respected opt-out)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { reviewEvasionProtection: "off" }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-off", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ n: number }>(); + expect(audit?.n).toBe(0); + }); + + it("does nothing when NO active review is tracked for this head (an ordinary close, nothing to evade)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + // No startActiveReviewTracking call at all. + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-active-review", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when a THIRD PARTY closed someone else's PR (not the author) — an ordinary maintainer close, not self-close evasion", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls, { collaboratorPermission: "write" }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-third-party", eventName: "pull_request", payload: closedPayload("a-maintainer", "contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the closer is the repo owner", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-owner", eventName: "pull_request", payload: closedPayload("JSONbored") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the closer is an ADMIN_GITHUB_LOGINS fleet-operator", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory", ADMIN_GITHUB_LOGINS: "admin-user" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-admin", eventName: "pull_request", payload: closedPayload("admin-user") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the closer holds write/maintain/admin collaborator permission", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls, { collaboratorPermission: "write" }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-maintainer", eventName: "pull_request", payload: closedPayload("write-collaborator") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing for a protected automation author (e.g. dependabot[bot])", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-bot", eventName: "pull_request", payload: closedPayload("dependabot[bot]") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("dry-run: audits the would-be enforcement without mutating GitHub or recording a live strike", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { agentDryRun: true }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-dry-run", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toContain("dry-run"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + }); + + it("denies enforcement when the agent is globally frozen", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await repositoriesModule.setGlobalAgentFrozen(env, true, "test"); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-frozen", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("paused"); + }); + + it("denies enforcement when close autonomy is not acting (observe)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { autonomy: { close: "observe" } }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-observe", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("autonomy for close is not acting"); + }); + + it("REGRESSION: denies live self-close enforcement when close autonomy requires approval", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-approval-required", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("requires approval"); + }); + + it("denies enforcement when pull_requests: write is not granted", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", publicSurface: "off", commentMode: "off", checkRunMode: "off", autonomy: { close: "auto" }, agentPaused: false, reviewEvasionProtection: "close" }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-write", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("pull_requests: write not granted"); + }); + + it("denies enforcement when the closed live PR is not on the reviewed head", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "closed", expectedHeadSha: "abc123", liveHeadSha: "def456", liveState: "closed" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-stale", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("review-evasion enforcement not executed"); + }); + + it("audits an error and does NOT record a strike when the reopen API call fails", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return new Response("server error", { status: 500 }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-reopen-fail", eventName: "pull_request", payload: closedPayload("contributor") }); + + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("error"); + expect(audit?.detail).toContain("FAILED to reopen"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + // The PR is closed either way (our reopen attempt failing doesn't reopen it) -- the general + // "closed"-action cleanup still terminalizes the tracking row, independent of enforcement success. + expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(false); + }); + + it("REGRESSION (gate-flagged): throws (never silently leaves the PR open) when reopen succeeds but the re-close API call fails, so the queue retries the job", async () => { + const calls: Array<{ url: string; method: string }> = []; + let patchCount = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) { + patchCount += 1; + if (patchCount === 1) return Response.json({ state: "open" }); // reopen succeeds + return new Response("server error", { status: 500 }); // re-close fails + } + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + // Deliberately UNCAUGHT: leaving the reopened PR open and returning normally would be worse than the + // contributor's original close, so this must propagate for the queue's own retry mechanism instead of + // resolving quietly. + await expect( + processJob(env, { type: "github-webhook", deliveryId: "self-close-close-fail", eventName: "pull_request", payload: closedPayload("contributor") }), + ).rejects.toThrow(); + + expect(patchCount).toBe(2); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("error"); + expect(audit?.detail).toContain("FAILED to re-close"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + // Still active -- enforcement never completed, so the active-review row must not have been cleared + // (the active-review-tracking cleanup below only fires on the "closed" webhook action's OWN pass, and + // this throw aborts that pass before it reaches the general terminalize hook). + expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(true); + }); + + it("REGRESSION (gate-flagged): a retry after the re-close failure converges -- the PR ends up closed, and the strike is recorded exactly once", async () => { + const calls: Array<{ url: string; method: string }> = []; + let closeAttempts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) { + const body = init?.body ? JSON.parse(String(init.body)) : {}; + if (body.state === "open") return Response.json({ state: "open" }); // reopen always succeeds + closeAttempts += 1; + if (closeAttempts === 1) return new Response("server error", { status: 500 }); // FIRST close attempt fails + return Response.json({ state: "closed" }); // retry's close attempt succeeds + } + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/labels")) return Response.json([{ name: "review-evasion" }]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", authorLogin: "contributor", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + const payload = closedPayload("contributor"); + await expect(processJob(env, { type: "github-webhook", deliveryId: "self-close-close-fail-retry", eventName: "pull_request", payload })).rejects.toThrow(); + // The queue's own retry mechanism re-delivers the SAME job after the first attempt threw. + await processJob(env, { type: "github-webhook", deliveryId: "self-close-close-fail-retry", eventName: "pull_request", payload }); + + expect(closeAttempts).toBe(2); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? order by created_at desc limit 1").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + const strikeCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strikeCount?.n).toBe(1); // exactly one strike, not one per attempt + expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(false); + }); + + it("global moderation disabled: the evasion close/label/comment still happen, but no moderation strike/label is recorded", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + // Global moderation config left at its default (disabled). + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-mod-off", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + }); + + it("REGRESSION: no duplicate strike or duplicate enforcement on a webhook redelivery/retry after the first enforcement already succeeded", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-redelivery-1", eventName: "pull_request", payload: closedPayload("contributor") }); + const firstPatchCount = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")).length; + expect(firstPatchCount).toBeGreaterThanOrEqual(2); + + // A SECOND, genuinely distinct delivery for the same underlying event (e.g. a queue retry after the first + // job's ack was lost) — the active-review row is already terminalized, so this must be a pure no-op. + await processJob(env, { type: "github-webhook", deliveryId: "self-close-redelivery-2", eventName: "pull_request", payload: closedPayload("contributor") }); + const secondPatchCount = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")).length - firstPatchCount; + expect(secondPatchCount).toBe(0); + + const strikeCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strikeCount?.n).toBe(1); + }); + + it("a subsequent contributor reopen after the App's evasion close is re-closed by the EXISTING one-shot reopen guard", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-then-reopen-1", eventName: "pull_request", payload: closedPayload("contributor") }); + expect((await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>())?.outcome).toBe("completed"); + + // getLastCloserLogin reads the issue-events timeline -- the App's own close (via the enforcement handler, + // NOT via the reopen-reclose guard) must be visible there for the existing guard to recognize it. + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); + if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "gittensory[bot]" } }, { event: "reopened", actor: { login: "contributor" } }]); + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 2 }, { status: 201 }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { type: "github-webhook", deliveryId: "contributor-reopens-after-evasion-close", eventName: "pull_request", payload: reopenedPayload("contributor") }); + + const reopenAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); + expect(reopenAudit?.outcome).toBe("completed"); + expect(reopenAudit?.detail).toContain("one-shot"); + }); + + it("STILL protects when reviewEvasionProtection is unset (undefined, not an explicit 'off') (#4011: default-ON)", async () => { + // upsertRepositorySettings coalesces undefined -> "close" at write time (mirrors reviewEvasionLabel/ + // reviewEvasionComment's own write-time defaulting below), and the consuming handler's own fallback + // (settings.reviewEvasionProtection === "off") treats anything but an explicit "off" as protected too -- + // so the only way to get `undefined` past BOTH layers and into the handler is to mock the resolved- + // settings layer directly, confirming neither layer silently reintroduces the old off-by-default gap. + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionProtection: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-protection-unset", eventName: "pull_request", payload: closedPayload("contributor") }); + + const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); + expect(patches.length).toBeGreaterThanOrEqual(2); // reopen then re-close, same as an explicit "close" + }); + + it("does nothing when the webhook payload has no sender", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + const payload = closedPayload("contributor"); + payload.sender = undefined; + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-sender", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the PR record has no author (a deleted-account PR)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + const payload = closedPayload("contributor"); + payload.pull_request.user = null; + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-author", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the PR record has no headSha", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + const payload = closedPayload("contributor"); + payload.pull_request.head = null; + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-head-sha", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("denies enforcement when the installation record is missing (uninstalled mid-flight)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + vi.spyOn(repositoriesModule, "getInstallation").mockResolvedValue(null); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-installation", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("pull_requests: write not granted"); + }); + + it("skips the courtesy comment when reviewEvasionComment is unset (defaults to true, but false is honored too)", async () => { + // Same write-time-coalescing note as the reviewEvasionProtection test above. + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionComment: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-comment-unset", eventName: "pull_request", payload: closedPayload("contributor") }); + + // reviewEvasionComment unset falls back to `true` -- the courtesy comment still posts. + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + }); + + it("applies no label when reviewEvasionLabel is explicitly null (a .gittensory.yml-only 'no label' override)", async () => { + const calls: Array<{ url: string; method: string }> = []; + const labelPostBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (method === "POST" && url.endsWith("/issues/42/labels")) { + labelPostBodies.push(String(init?.body ?? "")); + return Response.json([], { status: 200 }); + } + if (url.includes("/labels")) return Response.json([]); // dedup probe: no labels on the issue yet + if (url.includes("/pulls/42/files")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + // reviewEvasionLabel is a NOT NULL DB column (upsertRepositorySettings coalesces null -> the default at + // write time, per the migration's own "never persisted" comment) -- null only ever reaches this handler + // via the .gittensory.yml config-as-code layer, so the resolved-settings layer is mocked directly here. + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: null }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-label-null", eventName: "pull_request", payload: closedPayload("contributor") }); + + // Some OTHER unrelated feature (title-based type-labeling) may still post its own labels on a close -- + // what matters here is that the review-evasion label specifically was never requested. + expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(false); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + }); + + it("falls back to the default label when reviewEvasionLabel is unset", async () => { + const calls: Array<{ url: string; method: string }> = []; + const labelPostBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (method === "POST" && url.endsWith("/issues/42/labels")) { + labelPostBodies.push(String(init?.body ?? "")); + return Response.json([], { status: 200 }); + } + if (url.includes("/labels")) return Response.json([]); // dedup probe: no labels on the issue yet + if (url.includes("/pulls/42/files")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "self-close-label-unset", eventName: "pull_request", payload: closedPayload("contributor") }); + + expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(true); + }); + }); + + describe("converted_to_draft during an active review", () => { + it("closes as the App (no reopen needed), posts the explanation comment, applies the label, and records a review_evasion strike", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", authorLogin: "contributor", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); + expect(patches).toHaveLength(1); // no reopen needed -- a single close. + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toContain("draft-conversion"); + const strike = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ outcome: string }>(); + expect(strike?.outcome).toBe("completed"); + }); + + it("retries (via a thrown lock-contended error) when a concurrent delivery already holds the per-PR actuation lock", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + // Deliberately autonomy: {} (not {close: "auto"}) -- this repo's OUTER dispatch condition for the + // SIBLING draft-dodge guard requires isAgentConfigured(settings.autonomy), so with no acting autonomy + // class at all, draft-dodge's OWN lock-claim attempt is skipped entirely and this test genuinely + // exercises THIS handler's own lock claim/throw, not draft-dodge's (both guards fire on + // converted_to_draft and would otherwise race for the identical lock key). + await setupEvasionRepo(env, { autonomy: {} }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:jsonbored/gittensory#42", "1", 60); + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-lock-contended", eventName: "pull_request", payload: draftEvasionPayload("contributor") }), + ).rejects.toThrow("during review-evasion-draft"); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ n: number }>(); + expect(audit?.n).toBe(0); + }); + + it("does nothing for a draft conversion BEFORE any active review has started", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + // No startActiveReviewTracking call -- no review has ever run for this PR. + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-active-review", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does NOT require a prior gate failure (unlike the draft-dodge guard) -- an active review alone is enough", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + // Deliberately NO recordGateBlockOutcome call -- the draft-dodge guard's own trigger condition is absent. + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-gate-failure", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + const draftDodgeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ n: number }>(); + expect(draftDodgeAudit?.n).toBe(0); // the SIBLING guard never fired -- this is genuinely the new path. + const evasionAudit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); + expect(evasionAudit?.outcome).toBe("completed"); + }); + + it("does nothing when the author holds write collaborator permission", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls, { collaboratorPermission: "write" }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-maintainer", eventName: "pull_request", payload: draftEvasionPayload("write-collaborator") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("REGRESSION (gate-flagged): does nothing when a THIRD PARTY converts someone else's PR to draft (not the author) -- an ordinary maintainer action, not self-evasion, must never be enforced against the author who didn't do it", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls, { collaboratorPermission: "write" }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + const payload = draftEvasionPayload("contributor"); + payload.sender = { login: "a-maintainer", type: "User" }; // the CONVERTER, distinct from pull_request.user (the author) + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-third-party", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ n: number }>(); + expect(audit?.n).toBe(0); + }); + + it("dry-run: audits the would-be enforcement without mutating GitHub or recording a live strike", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { agentDryRun: true }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-dry-run", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toContain("dry-run"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + }); + + it("denies enforcement when the agent is globally frozen", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await repositoriesModule.setGlobalAgentFrozen(env, true, "test"); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-frozen", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("paused"); + }); + + it("denies enforcement when close autonomy is not acting (observe)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { autonomy: { close: "observe" } }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-observe", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("autonomy for close is not acting"); + }); + + it("REGRESSION: denies live draft-conversion enforcement when close autonomy requires approval", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-approval-required", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("requires approval"); + }); + + it("denies enforcement when pull_requests: write is not granted", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", publicSurface: "off", commentMode: "off", checkRunMode: "off", autonomy: { close: "auto" }, agentPaused: false, reviewEvasionProtection: "close" }); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-write", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("pull_requests: write not granted"); + }); + + it("denies enforcement when the PR was converted back to ready_for_review before the close fires (requireDraft freshness)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "no_longer_draft", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "open" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-longer-draft", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ requireDraft: true })); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + }); + + it("audits an error and does NOT record a strike when the close API call fails", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return new Response("server error", { status: 500 }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-close-fail", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("error"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + }); + + it("global moderation disabled: the evasion close/label/comment still happen, but no moderation strike is recorded", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-mod-off", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + }); + + it("STILL protects when reviewEvasionProtection is unset (undefined, not an explicit 'off') (#4011: default-ON)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionProtection: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-protection-unset", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + }); + + it("does nothing when the webhook payload has no sender", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + const payload = draftEvasionPayload("contributor"); + payload.sender = undefined; + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-sender", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the PR record has no author (a deleted-account PR)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + const payload = draftEvasionPayload("contributor"); + payload.pull_request.user = null; + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-author", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing for a protected automation author (e.g. dependabot[bot])", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-bot", eventName: "pull_request", payload: draftEvasionPayload("dependabot[bot]") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the PR record has no headSha", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + + const payload = draftEvasionPayload("contributor"); + payload.pull_request.head = null; + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-head-sha", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("denies enforcement when the installation record is missing (uninstalled mid-flight)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + vi.spyOn(repositoriesModule, "getInstallation").mockResolvedValue(null); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-installation", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("pull_requests: write not granted"); + }); + + it("skips the courtesy comment when reviewEvasionComment is unset (defaults to true, but false is honored too)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionComment: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-comment-unset", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + }); + + it("applies no label when reviewEvasionLabel is explicitly null (a .gittensory.yml-only 'no label' override)", async () => { + const calls: Array<{ url: string; method: string }> = []; + const labelPostBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (method === "POST" && url.endsWith("/issues/42/labels")) { + labelPostBodies.push(String(init?.body ?? "")); + return Response.json([], { status: 200 }); + } + if (url.includes("/labels")) return Response.json([]); // dedup probe: no labels on the issue yet + if (url.includes("/pulls/42/files")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: null }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-label-null", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(false); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + }); + + it("falls back to the default label when reviewEvasionLabel is unset", async () => { + const calls: Array<{ url: string; method: string }> = []; + const labelPostBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (method === "POST" && url.endsWith("/issues/42/labels")) { + labelPostBodies.push(String(init?.body ?? "")); + return Response.json([], { status: 200 }); + } + if (url.includes("/labels")) return Response.json([]); // dedup probe: no labels on the issue yet + if (url.includes("/pulls/42/files")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-label-unset", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(true); + }); + }); + + describe("repeated ready<->draft cycling (#gaming-tactic-draft-cycle)", () => { + it("does nothing on the FIRST draft conversion, then closes on the SECOND -- independent of active-review/gate-block state", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + // Deliberately NO startActiveReviewTracking / recordGateBlockOutcome call -- neither sibling guard's own + // trigger condition is present, so any close observed below can only be this new, count-based guard. + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); + expect(patches).toHaveLength(1); + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toContain("repeated draft-cycling"); + expect(audit?.detail).toContain("#2"); + const strike = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ outcome: string }>(); + expect(strike?.outcome).toBe("completed"); + }); + + it("does nothing when reviewEvasionProtection is off, even after a repeated cycle", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { reviewEvasionProtection: "off" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-off-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-off-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("STILL enforces the repeated-cycle close when reviewEvasionProtection is unset (undefined, not an explicit 'off') (#4011: default-ON)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionProtection: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-unset-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // first conversion never closes + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-unset-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); + expect(patches).toHaveLength(1); // second conversion closes, same as an explicit "close" + }); + + it("REGRESSION (gate-flagged): does not enforce against a THIRD PARTY repeatedly converting someone else's PR to draft", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls, { collaboratorPermission: "write" }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + const payload = draftEvasionPayload("contributor"); + payload.sender = { login: "a-maintainer", type: "User" }; + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-party-1", eventName: "pull_request", payload }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-party-2", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("REGRESSION (gate-flagged, gittensory-orb review): a maintainer's draft conversion must NOT count toward the author's own cycle -- the author's first-ever conversion is never enforced even after a prior third-party one", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls, { collaboratorPermission: "write" }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + const maintainerConversion = draftEvasionPayload("contributor"); + maintainerConversion.sender = { login: "a-maintainer", type: "User" }; + + // A maintainer converts the contributor's PR to draft first. + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-mixed-1", eventName: "pull_request", payload: maintainerConversion }); + // Without the fix, this maintainer action would have already bumped the shared counter to 1. + const afterMaintainer = await env.DB.prepare("select draft_conversion_count as n from pull_requests where repo_full_name = ? and number = 42") + .bind("JSONbored/gittensory") + .first<{ n: number }>(); + expect(afterMaintainer?.n).toBe(0); // the maintainer's own conversion never counted at all. + + // The AUTHOR now converts their OWN PR to draft for the very first time -- ordinary WIP behavior. + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-mixed-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const afterAuthor = await env.DB.prepare("select draft_conversion_count as n from pull_requests where repo_full_name = ? and number = 42") + .bind("JSONbored/gittensory") + .first<{ n: number }>(); + expect(afterAuthor?.n).toBe(1); // the author's first conversion is counted as their first, not their second. + }); + + it("does nothing for a protected automation author (e.g. dependabot[bot]), even after a repeated cycle", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-bot-1", eventName: "pull_request", payload: draftEvasionPayload("dependabot[bot]") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-bot-2", eventName: "pull_request", payload: draftEvasionPayload("dependabot[bot]") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the PR record has no headSha, even after a repeated cycle", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + const payload = draftEvasionPayload("contributor"); + payload.pull_request.head = null; + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-head-1", eventName: "pull_request", payload }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-head-2", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the author holds write collaborator permission, even after a repeated cycle", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls, { collaboratorPermission: "write" }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-maintainer-1", eventName: "pull_request", payload: draftEvasionPayload("write-collaborator") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-maintainer-2", eventName: "pull_request", payload: draftEvasionPayload("write-collaborator") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("denies enforcement when close autonomy is not acting (observe)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { autonomy: { close: "observe" } }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-observe-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-observe-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("autonomy for close is not acting"); + }); + + it("REGRESSION: denies live repeated draft-cycling enforcement when close autonomy requires approval", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-approval-required-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-approval-required-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("requires approval"); + }); + + it("dry-run: audits the would-be enforcement without mutating GitHub or recording a live strike", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { agentDryRun: true }); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-dry-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-dry-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toContain("dry-run"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + }); + + it("denies enforcement when the agent is paused for this repo", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { agentPaused: true }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-paused-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-paused-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("paused"); + }); + + it("denies enforcement when pull_requests: write is not granted", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertInstallation(env, { + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", publicSurface: "off", commentMode: "off", checkRunMode: "off", autonomy: { close: "auto" }, agentPaused: false, reviewEvasionProtection: "close" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-write-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-write-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("pull_requests: write not granted"); + }); + + it("denies enforcement when the PR was converted back to ready_for_review before the close fires (requireDraft freshness)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-fresh-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "no_longer_draft", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "open" }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-fresh-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ requireDraft: true })); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + }); + + it("audits an error and does NOT record a strike when the close API call fails", async () => { + const calls: Array<{ url: string; method: string }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return new Response("server error", { status: 500 }); + if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-close-fail-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-close-fail-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("error"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + }); + + it("global moderation disabled: the close/label/comment still happen, but no moderation strike is recorded", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-mod-off-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-mod-off-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); + expect(strike?.n).toBe(0); + }); + + it("REGRESSION: the third (and every later) conversion is enforced too, not just exactly the second", async () => { + const calls: Array<{ url: string; method: string }> = []; + let patchCount = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) { + patchCount += 1; + return Response.json({ state: "open" }); // simulate the close failing to stick / a reopen between cycles + } + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/labels")) return Response.json([{ name: "review-evasion" }]); + if (url.includes("/pulls/42/files")) return Response.json([]); + if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-3", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(patchCount).toBe(2); // enforced on the 2nd AND the 3rd -- >= 2, not === 2. + const completed = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and outcome = 'completed'") + .bind("github_app.review_evasion_closed") + .first<{ n: number }>(); + expect(completed?.n).toBe(2); + }); + + it("REGRESSION: the first conversion returns before the repeated-cycle lock so a retry cannot double-count it", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + // Deliberately autonomy: {} -- draft-dodge's own outer dispatch condition (isAgentConfigured) is false, so + // ITS lock claim never fires. The remaining sibling (review-evasion-active-review) has no settings gate at + // its OWN lock claim, so it claims+releases the lock normally on every converted_to_draft delivery. THIS + // guard now checks reviewEvasionProtection/count BEFORE claiming its own lock (#nit-lock-contention), so it + // never attempts a claim at all until draftConversionCount reaches 2 -- the first delivery below produces + // only the sibling's claim (mocked to succeed); the second produces the sibling's claim (succeeds) THEN + // this guard's own first-ever claim attempt, which is the one mocked to fail here. + await setupEvasionRepo(env, { autonomy: {} }); + const claimSpy = vi.spyOn(env.SELFHOST_TRANSIENT_CACHE!, "claim").mockResolvedValueOnce(true).mockResolvedValueOnce(true).mockResolvedValueOnce(false); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-lock-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + expect(claimSpy).toHaveBeenCalledTimes(1); // count is only 1 -- this guard never attempted a claim yet. + + await expect( + processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-lock-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }), + ).rejects.toThrow("during review-evasion-draft-cycle"); + + expect(claimSpy).toHaveBeenCalledTimes(3); + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the webhook payload has no sender, even after a repeated cycle", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + const payload = draftEvasionPayload("contributor"); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-sender-1", eventName: "pull_request", payload: { ...payload, sender: undefined } }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-sender-2", eventName: "pull_request", payload: { ...payload, sender: undefined } }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("does nothing when the PR record has no author (a deleted-account PR), even after a repeated cycle", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + const payload = draftEvasionPayload("contributor"); + payload.pull_request.user = null; + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-author-1", eventName: "pull_request", payload }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-author-2", eventName: "pull_request", payload }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + }); + + it("denies enforcement when the installation record is missing (uninstalled mid-flight)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-install-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + vi.spyOn(repositoriesModule, "getInstallation").mockResolvedValue(null); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-install-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") + .bind("github_app.review_evasion_closed") + .first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toContain("pull_requests: write not granted"); + }); + + it("skips the courtesy comment when reviewEvasionComment is explicitly false", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env, { reviewEvasionComment: false }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-comment-false-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-comment-false-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); + }); + + it("posts the courtesy comment when reviewEvasionComment is unset (undefined, not just a stored default)", async () => { + const calls: Array<{ url: string; method: string }> = []; + stubEvasionFetch(calls); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-comment-unset-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionComment: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-comment-unset-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); + }); + + it("applies no label when reviewEvasionLabel is explicitly null (a .gittensory.yml-only 'no label' override)", async () => { + const calls: Array<{ url: string; method: string }> = []; + const labelPostBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (method === "POST" && url.endsWith("/issues/42/labels")) { + labelPostBodies.push(String(init?.body ?? "")); + return Response.json([], { status: 200 }); + } + if (url.includes("/labels")) return Response.json([]); + if (url.includes("/pulls/42/files")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-label-null-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: null }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-label-null-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(false); + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? order by rowid desc limit 1").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); + }); + + it("falls back to the default label when reviewEvasionLabel is unset (undefined, not just a stored default)", async () => { + const calls: Array<{ url: string; method: string }> = []; + const labelPostBodies: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + calls.push({ url, method }); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); + if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); + if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); + if (method === "POST" && url.endsWith("/issues/42/labels")) { + labelPostBodies.push(String(init?.body ?? "")); + return Response.json([], { status: 200 }); + } + if (url.includes("/labels")) return Response.json([]); + if (url.includes("/pulls/42/files")) return Response.json([]); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + await setupEvasionRepo(env); + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-label-unset-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); + vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: undefined }); + + await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-label-unset-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); + + expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(true); + }); + }); + + describe("bumpPullRequestDraftConversionCount", () => { + it("increments across repeated calls for the same PR and is independent of head SHA", async () => { + const env = createTestEnv({}); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + id: 4242, + number: 77, + state: "open", + title: "Some PR", + user: { login: "contributor" }, + head: { sha: "sha-1", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: false, + merged: false, + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + } as never); + + expect(await repositoriesModule.bumpPullRequestDraftConversionCount(env, "JSONbored/gittensory", 77)).toBe(1); + expect(await repositoriesModule.bumpPullRequestDraftConversionCount(env, "JSONbored/gittensory", 77)).toBe(2); + // A fresh push (new head SHA) between cycles must NOT reset the counter -- unlike mergeAttemptCount. + await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + id: 4242, + number: 77, + state: "open", + title: "Some PR", + user: { login: "contributor" }, + head: { sha: "sha-2", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: false, + merged: false, + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + } as never); + expect(await repositoriesModule.bumpPullRequestDraftConversionCount(env, "JSONbored/gittensory", 77)).toBe(3); + }); + + it("returns 0 for a PR that does not exist (no row to increment)", async () => { + const env = createTestEnv({}); + expect(await repositoriesModule.bumpPullRequestDraftConversionCount(env, "JSONbored/gittensory", 999999)).toBe(0); + }); + }); +}); + +describe("markPullRequestLinkedIssueHardRuleViolated (#linked-issue-hard-rule-persistence)", () => { + it("sets violatedAt + the reason on the first call and never overwrites them on a later call", async () => { + const env = createTestEnv({}); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + id: 5151, + number: 88, + state: "open", + title: "Some PR", + user: { login: "contributor" }, + head: { sha: "sha-1", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: false, + merged: false, + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + } as never); + + const before = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 88); + expect(before?.linkedIssueHardRuleViolatedAt).toBeNull(); + expect(before?.linkedIssueHardRuleViolationReason).toBeNull(); + + await repositoriesModule.markPullRequestLinkedIssueHardRuleViolated(env, "JSONbored/gittensory", 88, "Linked issue #7 is assigned to the maintainer (@JSONbored)"); + const afterFirst = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 88); + expect(afterFirst?.linkedIssueHardRuleViolatedAt).toEqual(expect.any(String)); + expect(afterFirst?.linkedIssueHardRuleViolationReason).toBe("Linked issue #7 is assigned to the maintainer (@JSONbored)"); + + // A SECOND confirmed violation (e.g. against a different linked issue, or a re-detected same one) must not + // move the timestamp or replace the reason -- the FIRST confirmed violation is what's remembered forever. + await repositoriesModule.markPullRequestLinkedIssueHardRuleViolated(env, "JSONbored/gittensory", 88, "Linked issue #9 is already assigned to @someone-else"); + const afterSecond = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 88); + expect(afterSecond?.linkedIssueHardRuleViolatedAt).toBe(afterFirst?.linkedIssueHardRuleViolatedAt); + expect(afterSecond?.linkedIssueHardRuleViolationReason).toBe("Linked issue #7 is assigned to the maintainer (@JSONbored)"); + + // A fresh push (new head SHA) between violations must NOT reset either field -- unlike mergeBlockedSha, + // this marker is deliberately not scoped to head SHA. + await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + id: 5151, + number: 88, + state: "open", + title: "Some PR", + user: { login: "contributor" }, + head: { sha: "sha-2", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: false, + merged: false, + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + } as never); + const afterNewHead = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 88); + expect(afterNewHead?.linkedIssueHardRuleViolatedAt).toBe(afterFirst?.linkedIssueHardRuleViolatedAt); + expect(afterNewHead?.linkedIssueHardRuleViolationReason).toBe("Linked issue #7 is assigned to the maintainer (@JSONbored)"); + }); + + it("truncates an overlong reason to 280 chars, mirroring markPullRequestMergeBlocked", async () => { + const env = createTestEnv({}); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + id: 5152, + number: 89, + state: "open", + title: "Some PR", + user: { login: "contributor" }, + head: { sha: "sha-1", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: false, + merged: false, + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + } as never); + + const longReason = "x".repeat(400); + await repositoriesModule.markPullRequestLinkedIssueHardRuleViolated(env, "JSONbored/gittensory", 89, longReason); + const row = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 89); + expect(row?.linkedIssueHardRuleViolationReason).toHaveLength(280); + }); + + it("is a safe no-op when the PR row does not exist yet", async () => { + const env = createTestEnv({}); + await expect(repositoriesModule.markPullRequestLinkedIssueHardRuleViolated(env, "JSONbored/gittensory", 999999, "unreachable")).resolves.toBeUndefined(); + }); +}); + +describe("recordAgentCommandUsage (signal-snapshot fail-safe)", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("swallows persistSignalSnapshot errors — catch body runs without crashing the handler", async () => { + // Bot-authored @gittensory comment hits the early bot_author bail-out path in + // maybeProcessGittensoryMentionCommand, which calls recordAgentCommandUsage. Injecting a + // persistSignalSnapshot failure exercises the catch at the bottom of that function. + vi.spyOn(repositoriesModule, "persistSignalSnapshot").mockRejectedValueOnce(new Error("signal DB error")); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + const payload: any = { + action: "created", + installation: { id: 123 }, + repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, + sender: { login: "gittensory[bot]", type: "Bot" }, + comment: { id: 999, body: "@gittensory help", user: { login: "gittensory[bot]", type: "Bot" } }, + issue: { id: 1, number: 77, title: "some issue", pull_request: { url: "https://api.github.com/repos/JSONbored/gittensory/pulls/77" } }, + }; + await expect( + processJob(env, { type: "github-webhook", deliveryId: "bot-mention-signal-fail", eventName: "issue_comment", payload }), + ).resolves.toBeUndefined(); + }); + + it("ignores a @gittensory mention on an EDITED comment — only newly-created comments are answered (#review-audit)", async () => { + const posts: string[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if ((init?.method ?? "GET") === "POST" && url.includes("/comments")) posts.push(url); + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + return new Response("not found", { status: 404 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); + const payload: any = { + action: "edited", // an edit re-fires issue_comment with a NEW delivery id — the handler must NOT re-answer + installation: { id: 123 }, + repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, + sender: { login: "maintainer", type: "User" }, + comment: { id: 999, body: "@gittensory ask is this mergeable?", user: { login: "maintainer", type: "User" } }, + issue: { id: 1, number: 77, title: "some issue", pull_request: { url: "https://api.github.com/repos/JSONbored/gittensory/pulls/77" } }, + }; + await processJob(env, { type: "github-webhook", deliveryId: "mention-edited", eventName: "issue_comment", payload }); + expect(posts).toEqual([]); // the action guard returns false → no answer card posted + }); +}); + +function generateRsaPrivateKeyPem(): string { + const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + return privateKey.export({ type: "pkcs1", format: "pem" }).toString(); +} + +function reopenedPayload(sender: string): any { + return { + action: "reopened", + installation: { id: 123 }, + repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, + sender: { login: sender, type: "User" }, + pull_request: { + id: 4242, + number: 42, + state: "open", + title: "Fix queued guard", + body: "Fixes the queued guard.", + user: { login: "contributor" }, + head: { sha: "abc123", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, + base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, + draft: false, + merged: false, + mergeable_state: "clean", + created_at: "2026-05-27T00:00:00Z", + updated_at: "2026-05-27T00:00:00Z", + }, + }; +} + +describe("installation app_id capture + dual-app webhook filter (#selfhost-app-id)", () => { + it("captures app_id from an installation payload, returns it, and preserves it when a later payload omits it", async () => { + const env = createTestEnv(); + const stored = await upsertInstallation(env, { + action: "created", + installation: { id: 4242, app_id: 555, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] }, + }); + expect(stored).toBe(555); + expect((await getInstallation(env, 4242))?.appId).toBe(555); + // A subsequent payload WITHOUT app_id (e.g. a pull_request event) must not clear the stored value. + const preserved = await upsertInstallation(env, { action: "synchronize", installation: { id: 4242, account: { login: "owner", id: 1, type: "Organization" } } }); + expect(preserved).toBe(555); + expect((await getInstallation(env, 4242))?.appId).toBe(555); + }); + + it("acks a webhook whose installation belongs to a DIFFERENT app without processing it", async () => { + const env = createTestEnv(); // own GITHUB_APP_ID defaults to "3824093" + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 7777); + // The installation is recorded as belonging to a FOREIGN app (99999 ≠ 3824093). + await upsertInstallation(env, { action: "created", installation: { id: 7777, app_id: 99999, account: { login: "JSONbored", id: 1, type: "User" }, repository_selection: "selected", permissions: {}, events: [] } }); + vi.stubGlobal("fetch", async () => Response.json({})); + + await processJob(env, { + type: "github-webhook", + deliveryId: "foreign-app-pr", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 7777 }, // a PR event carries no app_id; the stored 99999 is used + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 88, title: "Foreign", state: "open", user: { login: "contributor" }, head: { sha: "f88" }, labels: [], body: "x" }, + }, + }); + + // The delivery was acked as foreign, and the PR was never upserted (the handler returned before the PR block). + const evt = await env.DB.prepare("select payload_hash from webhook_events where delivery_id = ?").bind("foreign-app-pr").first<{ payload_hash: string }>(); + expect(evt?.payload_hash).toBe("foreign_app"); + const pr = await env.DB.prepare("select count(*) as n from pull_requests where repo_full_name = ? and number = ?").bind("JSONbored/gittensory", 88).first<{ n: number }>(); + expect(pr?.n).toBe(0); + }); + + it("processes a webhook whose installation app_id matches this backend (no false filtering)", async () => { + const env = createTestEnv(); // own GITHUB_APP_ID "3824093" + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 3824093001); + await upsertInstallation(env, { action: "created", installation: { id: 3824093001, app_id: 3824093, account: { login: "JSONbored", id: 1, type: "User" }, repository_selection: "selected", permissions: {}, events: [] } }); + vi.stubGlobal("fetch", async () => Response.json({})); + + await processJob(env, { + type: "github-webhook", + deliveryId: "own-app-pr", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 3824093001 }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 89, title: "Own", state: "open", user: { login: "contributor" }, head: { sha: "o89" }, labels: [], body: "x" }, + }, + }); + + // The matching-app webhook was processed normally — the PR row exists and it was NOT acked as foreign. + const pr = await env.DB.prepare("select count(*) as n from pull_requests where repo_full_name = ? and number = ?").bind("JSONbored/gittensory", 89).first<{ n: number }>(); + expect(pr?.n).toBe(1); + const evt = await env.DB.prepare("select payload_hash from webhook_events where delivery_id = ?").bind("own-app-pr").first<{ payload_hash: string }>(); + expect(evt?.payload_hash).not.toBe("foreign_app"); + }); + + // #2537: durable PR-state cache — webhook invalidation + the act-boundary regression. + describe("durable PR-state cache (#2537)", () => { + function seedWarmPrStateCache(env: Env, repoFullName: string, pullNumber: number): Promise { + return upsertPullRequestDetailSyncState(env, { + repoFullName, + pullNumber, + status: "complete", + prMergeableState: "clean", + prState: "open", + prStateFetchedAt: new Date().toISOString(), + }); + } + + it.each(["synchronize", "closed", "reopened"] as const)( + "pull_request %s action invalidates the durable PR-state cache", + async (action) => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await seedWarmPrStateCache(env, "JSONbored/gittensory", 200); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "tok" }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: `invalidate-pr-state-${action}`, + eventName: "pull_request", + payload: { + action, + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 200, title: "PR", state: action === "closed" ? "closed" : "open", user: { login: "contributor" }, head: { sha: "a200" }, labels: [], body: "" }, + }, + }); + + expect(await getPullRequestDetailSyncState(env, "JSONbored/gittensory", 200)).toMatchObject({ + prMergeableState: null, + prState: null, + prStateFetchedAt: null, + }); + }, + ); + + it("a non-invalidating pull_request action (labeled) leaves the durable PR-state cache UNCHANGED", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await seedWarmPrStateCache(env, "JSONbored/gittensory", 201); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "tok" }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "invalidate-pr-state-labeled", + eventName: "pull_request", + payload: { + action: "labeled", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 201, title: "PR", state: "open", user: { login: "contributor" }, head: { sha: "a201" }, labels: [], body: "" }, + }, + }); + + expect(await getPullRequestDetailSyncState(env, "JSONbored/gittensory", 201)).toMatchObject({ + prMergeableState: "clean", + prState: "open", + }); + }); + + it("REGRESSION (#2537, gate-flagged): reconcileLiveDuplicateSiblings must NOT serve a warm durable PR-state cache row — a cached 'open' read up to PR_STATE_CACHE_MAX_AGE_MS stale after a missed closed webhook would keep an already-closed sibling eligible as the duplicate-cluster winner, wrongly closing the CURRENT PR as the loser", async () => { + const env = createTestEnv({ GITTENSORY_DUPLICATE_WINNER: "true" }); + // Seed a WARM cache row claiming the sibling is still open, but the live GitHub state below says CLOSED — + // proving the cache is never consulted: only a genuine live read can discover this and correctly reconcile it. + await seedWarmPrStateCache(env, "owner/repo", 5); + let liveStateFetches = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "tok" }); + if (/\/pulls\/5(?:\?|$)/.test(url)) { + liveStateFetches += 1; + return Response.json({ number: 5, state: "closed" }); + } + return Response.json({}); + }); + + const winner: Parameters[3] = { repoFullName: "owner/repo", number: 10, title: "Winner", state: "open", labels: [], linkedIssues: [1] }; + const sibling: Parameters[3] = { repoFullName: "owner/repo", number: 5, title: "Sibling", state: "open", labels: [], linkedIssues: [1] }; + const result = await reconcileLiveDuplicateSiblings(env, null, "owner/repo", winner, [sibling]); + + // The sibling is correctly dropped as stale-closed, proving a genuine live fetch happened rather than + // trusting the warm-but-wrong cached "open" value. + expect(result).toEqual([]); + expect(liveStateFetches).toBe(1); + }); + + it("REGRESSION (#2537): the per-PR sweep unit's live resync primes the durable PR-state cache for later readers", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { pull_requests: "write" }, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 6, title: "Sweep target", state: "open", user: { login: "contributor" }, head: { sha: "a6" }, base: { ref: "main" }, labels: [], body: "" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "tok" }); + if (/\/pulls\/6(?:\?|$)/.test(url)) return Response.json({ number: 6, state: "open", mergeable_state: "clean", head: { sha: "a6" } }); + if (url.includes("/pulls/6/files")) return Response.json([]); + if (url.includes("/pulls/6/reviews")) return Response.json([]); + if (url.includes("/commits/a6/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a6/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "prime-pr-state-cache", repoFullName: "owner/agent-repo", prNumber: 6, installationId: 9001 }); + + expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 6)).toMatchObject({ + prMergeableState: "clean", + prState: "open", + }); + }); + }); +}); + +describe("enrichOpenPullRequestsWithChangedFiles (#2653)", () => { + const pr = (number: number, overrides: Partial = {}): PullRequestRecord => ({ + repoFullName: "owner/repo", + number, + title: `PR ${number}`, + state: "open", + labels: [], + linkedIssues: [], + ...overrides, + }); + + it("populates changedFiles for open PRs from the pull_request_files cache", async () => { + const env = createTestEnv(); + await upsertPullRequestFile(env, { repoFullName: "owner/repo", pullNumber: 10, path: "src/a.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + await upsertPullRequestFile(env, { repoFullName: "owner/repo", pullNumber: 10, path: "src/b.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + await upsertPullRequestFile(env, { repoFullName: "owner/repo", pullNumber: 11, path: "src/c.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + + const result = await enrichOpenPullRequestsWithChangedFiles(env, "owner/repo", [pr(10), pr(11)]); + + expect(result.find((candidate) => candidate.number === 10)?.changedFiles?.sort()).toEqual(["src/a.ts", "src/b.ts"]); + expect(result.find((candidate) => candidate.number === 11)?.changedFiles).toEqual(["src/c.ts"]); + }); + + it("leaves a PR's changedFiles untouched when the cache has no rows for it (fail-safe degrade, not an error)", async () => { + const env = createTestEnv(); + await upsertPullRequestFile(env, { repoFullName: "owner/repo", pullNumber: 10, path: "src/a.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); + + const result = await enrichOpenPullRequestsWithChangedFiles(env, "owner/repo", [pr(10), pr(12)]); + + expect(result.find((candidate) => candidate.number === 12)?.changedFiles).toBeUndefined(); + }); + + it("does not query the cache and returns the same array reference when there are no open PRs", async () => { + const env = createTestEnv(); + const input = [pr(20, { state: "closed" })]; + + const result = await enrichOpenPullRequestsWithChangedFiles(env, "owner/repo", input); + + expect(result).toBe(input); + }); + + it("returns the same array reference when the cache has no rows for any open PR", async () => { + const env = createTestEnv(); + const input = [pr(30)]; + + const result = await enrichOpenPullRequestsWithChangedFiles(env, "owner/repo", input); + + expect(result).toBe(input); + }); +}); + +describe("backlog-convergence sweep (#selfhost-backlog-convergence)", () => { + it("fans out to acting-autonomy repos, skipping a non-acting/non-allowlisted repo", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITTENSORY_REVIEW_REPOS: "", + JOBS: { async send(message: import("../../src/types").JobMessage) { sent.push(message); } } as unknown as Queue, + }); + await upsertRepositoryFromGitHub(env, { name: "agent-a", full_name: "owner/agent-a", private: false, owner: { login: "owner" } }); + await upsertRepositoryFromGitHub(env, { name: "plain-repo", full_name: "owner/plain-repo", private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-a", autonomy: { merge: "auto" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/plain-repo", autonomy: { review: "observe" } }); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); + + expect(sent).toHaveLength(1); + expect(sent[0]).toMatchObject({ type: "backlog-convergence-sweep", repoFullName: "owner/agent-a" }); + const fanout = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?") + .bind("agent.sweep.backlog_convergence.fanout") + .first<{ outcome: string; metadata_json: string }>(); + expect(fanout?.outcome).toBe("queued"); + expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 1, requestedBy: "schedule" }); + }); + + it("also fans out to an allowlisted repo regardless of autonomy mode (#sweep-all-modes parity)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ GITTENSORY_REVIEW_REPOS: "owner/advisory-repo", JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertRepositoryFromGitHub(env, { name: "advisory-repo", full_name: "owner/advisory-repo", private: false, owner: { login: "owner" } }, 9502); + await upsertRepositorySettings(env, { repoFullName: "owner/advisory-repo", autonomy: { merge: "observe" } }); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); + + expect(sent).toEqual([expect.objectContaining({ type: "backlog-convergence-sweep", repoFullName: "owner/advisory-repo", installationId: 9502 })]); + }); + + it("fans out to an allowlisted repo that was never registered locally (no installationId) and staggers a second repo's delay", async () => { + const sent: Array<{ message: import("../../src/types").JobMessage; delaySeconds?: number }> = []; + const env = createTestEnv({ + GITTENSORY_REVIEW_REPOS: "owner/never-registered", + JOBS: { async send(m: import("../../src/types").JobMessage, options?: { delaySeconds?: number }) { sent.push({ message: m, ...(options?.delaySeconds === undefined ? {} : { delaySeconds: options.delaySeconds }) }); } } as unknown as Queue, + }); + await upsertRepositoryFromGitHub(env, { name: "agent-a", full_name: "owner/agent-a", private: false, owner: { login: "owner" } }, 9506); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-a", autonomy: { merge: "auto" } }); + // owner/never-registered is allowlisted but has no local repository row at all -> no installationId to attach. + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); + + expect(sent).toHaveLength(2); + const neverRegistered = sent.find((s) => s.message.type === "backlog-convergence-sweep" && s.message.repoFullName === "owner/never-registered"); + expect(neverRegistered?.message).not.toHaveProperty("installationId"); + // Whichever entry landed second (index 1) carries a nonzero stagger delay. + expect(sent.some((s) => (s.delaySeconds ?? 0) > 0)).toBe(true); + }); + + it("no-ops safely on a missing repo arg or an un-configured repo", async () => { + const env = createTestEnv({}); + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test" }); + await upsertRepositoryFromGitHub(env, { name: "plain-repo", full_name: "owner/plain-repo", private: false, owner: { login: "owner" } }); + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/plain-repo" }); + + const count = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence").first<{ n: number }>(); + expect(count?.n).toBe(0); + }); + + it("respects the global pause kill-switch: a paused repo records a denial and enqueues nothing", async () => { + const env = createTestEnv({}); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9503); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, agentPaused: true }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale surface", state: "open", user: { login: "contributor" }, head: { sha: "abc" }, labels: [], body: "x" }); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const audit = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?") + .bind("agent.sweep.backlog_convergence") + .first<{ outcome: string; detail: string; metadata_json: string }>(); + expect(audit?.outcome).toBe("denied"); + expect(audit?.detail).toMatch(/paused/i); + expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ mode: "paused" }); + }); + + it("stays quiet (no audit, no enqueue) with no installation to act with", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }); // no installationId + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale surface", state: "open", user: { login: "contributor" }, head: { sha: "abc" }, labels: [], body: "x" }); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + expect(sent).toEqual([]); + const count = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence").first<{ n: number }>(); + expect(count?.n).toBe(0); + }); + + it("stays quiet when every open PR's surface is already published at its current head", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9504, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9504); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Converged", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "x" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + expect(sent).toEqual([]); + }); + + it("fans out one agent-regate-pr per stale-surface candidate, tagged with the backlog-convergence deliveryId prefix", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9505, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9505); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + // #7 never had its surface published; #8 was published at an OLDER head than its current one; #9 is fully converged; + // #10 is a legacy/sparse row with no GitHub created_at, and still needs a re-gate without PR-age metadata. + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Never published", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "x", created_at: "2026-07-03T10:00:00.000Z" }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 8, title: "Stale surface", state: "open", user: { login: "contributor" }, head: { sha: "b8" }, labels: [], body: "x", created_at: "2026-07-03T11:00:00.000Z" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 8, "old-b8"); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 9, title: "Converged", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "x" }); + await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 9, "a9"); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 10, title: "Sparse legacy row", state: "open", user: { login: "contributor" }, head: { sha: "a10" }, labels: [], body: "x" }); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); + + const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); + expect(fanned.map((job) => job.prNumber).sort((a, b) => a - b)).toEqual([7, 8, 10]); + for (const job of fanned) { + expect(job.deliveryId).toBe(`backlog-convergence:owner/agent-repo#${job.prNumber}`); + expect(job.installationId).toBe(9505); + } + expect(Object.fromEntries(fanned.map((job) => [job.prNumber, job.prCreatedAt]))).toEqual({ + 7: "2026-07-03T10:00:00.000Z", + 8: "2026-07-03T11:00:00.000Z", + 10: undefined, + }); + const audit = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?") + .bind("agent.sweep.backlog_convergence") + .first<{ outcome: string; detail: string; metadata_json: string }>(); + expect(audit?.outcome).toBe("completed"); + const meta = JSON.parse(audit?.metadata_json ?? "{}"); + expect(meta).toMatchObject({ repoFullName: "owner/agent-repo", openCount: 4, examined: 3 }); + expect(meta.candidatePulls.sort((a: number, b: number) => a - b)).toEqual([7, 8, 10]); + }); + + it("REGRESSION (#4502, #audit-sweep-dispatch-stamp): ONE sweep stamps ALL candidates AT DISPATCH, so the next fan-out skips the repo as draining — no overlapping sweeps", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9510, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9510); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + for (const number of [7, 8, 9]) { + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `PR${number}`, state: "open", user: { login: "c" }, head: { sha: `a${number}` }, labels: [], body: "" }); + } + + // Run ONE per-repo sweep — do NOT drain the per-PR jobs (simulate the staggered re-reviews not having run yet). + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + // The marker is stamped for EVERY candidate immediately at dispatch — NOT waiting on the per-PR jobs. + const stamped = await env.DB.prepare("select count(*) as n from pull_requests where repo_full_name = ? and last_backlog_convergence_regated_at is not null").bind("owner/agent-repo").first<{ n: number }>(); + expect(stamped?.n).toBe(3); + + // So the very next cron fan-out sees the fresh stamp and SKIPS this repo as draining — the overlap that would + // duplicate per-PR jobs is gone. + sent.length = 0; + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); + expect(sent.some((m) => m.type === "backlog-convergence-sweep" && m.repoFullName === "owner/agent-repo")).toBe(false); + const fanout = await env.DB.prepare("select metadata_json from audit_events where event_type = ? order by created_at desc limit 1").bind("agent.sweep.backlog_convergence.fanout").first<{ metadata_json: string }>(); + expect(JSON.parse(fanout?.metadata_json ?? "{}").skippedDraining).toBeGreaterThanOrEqual(1); + }); + + it("INVARIANT (#4502, in-flight guard): the fan-out SKIPS a repo whose prior sweep is still draining, enqueues an idle one", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ GITTENSORY_REVIEW_REPOS: "", JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9511, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + for (const name of ["draining", "idle"]) { + await upsertRepositoryFromGitHub(env, { name, full_name: `owner/${name}`, private: false, owner: { login: "owner" } }, 9511); + await upsertRepositorySettings(env, { repoFullName: `owner/${name}`, autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, `owner/${name}`, { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "h1" }, labels: [], body: "" }); + } + // owner/draining was just backlog-convergence-regated (a sweep is mid-drain); owner/idle has never been swept. + await repositoriesModule.markPullRequestsBacklogConvergenceRegated(env, "owner/draining", [1]); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); // no repoFullName → fan-out path + + const sweepRepos = sent.filter((m): m is Extract => m.type === "backlog-convergence-sweep").map((m) => m.repoFullName); + expect(sweepRepos).toEqual(["owner/idle"]); // the draining repo is skipped, the idle one enqueued + const fanout = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence.fanout").first<{ metadata_json: string }>(); + expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 1, skippedDraining: 1 }); + }); + + it("INVARIANT (#4502, #audit-fanout-dedup): a BURST of fan-outs collapses to ONE — the second claims nothing and audits denied", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9512, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9512); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); // first fan-out claims the window + expect(sent.some((m) => m.type === "backlog-convergence-sweep" && m.repoFullName === "owner/agent-repo")).toBe(true); + + sent.length = 0; + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); // burst sibling in the same window → deduped + expect(sent.filter((m) => m.type === "backlog-convergence-sweep")).toEqual([]); // enqueues no redundant sweep + const denied = await env.DB.prepare("select count(*) as n from audit_events where event_type='agent.sweep.backlog_convergence.fanout' and outcome='denied'").first<{ n: number }>(); + expect(denied?.n).toBe(1); + }); + + it("REGRESSION (#4502, #audit-sweep-fanout-isolation): one repo's settings-check failure does not abort the fan-out for every other repo", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITTENSORY_REVIEW_REPOS: "", + JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue, + }); + await upsertRepositoryFromGitHub(env, { name: "agent-a", full_name: "owner/agent-a", private: false, owner: { login: "owner" } }); + await upsertRepositoryFromGitHub(env, { name: "agent-b", full_name: "owner/agent-b", private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-a", autonomy: { label: "auto" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-b", autonomy: { label: "auto" } }); + const realResolve = repositorySettingsModule.resolveRepositorySettings; + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + const resolveSpy = vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockImplementation(async (e, repoFullName) => { + if (repoFullName === "owner/agent-a") throw new Error("D1 read error"); + return realResolve(e, repoFullName); + }); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); + + expect(sent).toEqual([expect.objectContaining({ type: "backlog-convergence-sweep", repoFullName: "owner/agent-b" })]); // agent-a's failure did not block agent-b + expect(errors.mock.calls.some((call) => String(call[0]).includes("backlog_convergence_fanout_repo_check_failed") && String(call[0]).includes("owner/agent-a"))).toBe(true); + const fanout = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence.fanout").first<{ outcome: string; metadata_json: string }>(); + expect(fanout?.outcome).toBe("queued"); // the fan-out still completes and records its own outcome + expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 1, skippedErrored: 1 }); + errors.mockRestore(); + resolveSpy.mockRestore(); + }); + + it("REGRESSION (#4502, #audit-sweep-fanout-isolation): one repo's dispatch failure does not abort dispatch for every other repo, and the fan-out audit event still records", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITTENSORY_REVIEW_REPOS: "", + JOBS: { + async send(m: import("../../src/types").JobMessage) { + if (m.type === "backlog-convergence-sweep" && m.repoFullName === "owner/agent-a") throw new Error("queue send error"); + sent.push(m); + }, + } as unknown as Queue, + }); + await upsertRepositoryFromGitHub(env, { name: "agent-a", full_name: "owner/agent-a", private: false, owner: { login: "owner" } }); + await upsertRepositoryFromGitHub(env, { name: "agent-b", full_name: "owner/agent-b", private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-a", autonomy: { label: "auto" } }); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-b", autonomy: { label: "auto" } }); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); + + expect(sent).toEqual([expect.objectContaining({ type: "backlog-convergence-sweep", repoFullName: "owner/agent-b" })]); // agent-a's failed send did not block agent-b's + expect(errors.mock.calls.some((call) => String(call[0]).includes("backlog_convergence_fanout_dispatch_failed") && String(call[0]).includes("owner/agent-a"))).toBe(true); + const fanout = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence.fanout").first<{ outcome: string; metadata_json: string }>(); + expect(fanout?.outcome).toBe("queued"); // reached — the dispatch failure did not throw the fan-out itself + expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 2 }); // both PASSED their settings/draining checks regardless of dispatch outcome + errors.mockRestore(); + }); + + it("agent re-gate sweep swallows a failing last_backlog_convergence_regated_at stamp and still completes (#4502, #audit-sweep-converge)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); + await upsertInstallation(env, { action: "created", installation: { id: 9513, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9513); + await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); + await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale surface", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + const stamp = vi.spyOn(repositoriesModule, "markPullRequestsBacklogConvergenceRegated").mockRejectedValueOnce(new Error("D1 write error")); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence").first<{ outcome: string }>(); + expect(audit?.outcome).toBe("completed"); // the sweep still completes; the dispatch-time stamp failure is swallowed + expect(sent.some((m) => m.type === "agent-regate-pr" && m.prNumber === 7)).toBe(true); // the per-PR fan-out still happens + expect(errors.mock.calls.some((call) => String(call[0]).includes("backlog_convergence_mark_regated_failed"))).toBe(true); + stamp.mockRestore(); + errors.mockRestore(); + }); + + it("REGRESSION (#4502, #3899-style port): resolves multiple repos' settings/drain-state CONCURRENTLY, bounded by SWEEP_FANOUT_RESOLUTION_CONCURRENCY, and drops no repo", async () => { + vi.useRealTimers(); + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + GITTENSORY_REVIEW_REPOS: "", + JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue, + }); + const repoNames = ["r1", "r2", "r3", "r4", "r5", "r6"]; + for (const name of repoNames) { + await upsertRepositoryFromGitHub(env, { name, full_name: `owner/${name}`, private: false, owner: { login: "owner" } }); + await upsertRepositorySettings(env, { repoFullName: `owner/${name}`, autonomy: { merge: "auto" } }); + } + const { mapWithConcurrencyLimit: realMapWithConcurrencyLimit } = + await vi.importActual("../../src/signals/focus-manifest-loader"); + let inFlight = 0; + let maxInFlight = 0; + const mapSpy = vi.spyOn(focusManifestLoaderModule, "mapWithConcurrencyLimit").mockImplementation( + async (items, limit, mapper) => { + expect(limit).toBe(SWEEP_FANOUT_RESOLUTION_CONCURRENCY); + return realMapWithConcurrencyLimit(items, limit, async (item) => { + inFlight += 1; + maxInFlight = Math.max(maxInFlight, inFlight); + try { + await new Promise((resolve) => setTimeout(resolve, 5)); // hold the window open long enough for others to overlap + return await mapper(item); + } finally { + inFlight -= 1; + } + }); + }, + ); + + await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); + + expect(mapSpy).toHaveBeenCalled(); + expect(maxInFlight).toBeGreaterThan(1); // proves real overlap — not the old strictly-sequential loop + expect(maxInFlight).toBeLessThanOrEqual(SWEEP_FANOUT_RESOLUTION_CONCURRENCY); // proves BOUNDED, not unlimited fan-out + expect(sent.filter((m) => m.type === "backlog-convergence-sweep").length).toBe(repoNames.length); // every repo still dispatched, none silently dropped + }); +}); + +// #selfhost-auto-action-convergence: end-to-end regression coverage for the GENERAL heuristic plan+execute path +// (runAgentMaintenancePlanAndExecute -> planAgentMaintenanceActions -> executeAgentMaintenanceActions), via real +// webhook -> processJob -> mocked-GitHub-API assertions. The specialized short-circuit mechanisms (blacklist, +// contributor-cap, review-nag, converted_to_draft gate-close) already have deep end-to-end coverage elsewhere in +// this file; planAgentMaintenanceActions itself is exhaustively unit-tested in agent-actions.test.ts; and +// executeAgentMaintenanceActions's own gate stack is exhaustively unit-tested in agent-action-executor.test.ts. +// What was missing was END-TO-END proof, for the plain gate-verdict path specifically, that the two connect: a +// plan computed from REAL PR/settings state actually reaches a REAL (mocked) GitHub mutation. +describe("auto-action convergence: end-to-end plan+execute for the general heuristic path (#selfhost-auto-action-convergence)", () => { + const REPO = "JSONbored/gittensory"; + const INSTALLATION_ID = 9600; + + beforeEach(() => clearInstallationTokenCacheForTest()); + afterEach(() => { + clearInstallationTokenCacheForTest(); + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + async function setupAutoActionRepo(env: ReturnType, settingsOverrides: Record = {}): Promise { + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }, INSTALLATION_ID); + await upsertInstallation(env, { + installation: { + id: INSTALLATION_ID, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", contents: "write", pull_requests: "write", issues: "write" }, + events: ["pull_request"], + }, + repositories: [{ name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }], + }); + await upsertRepositorySettings(env, { + repoFullName: REPO, + commentMode: "off", + publicSurface: "off", + checkRunMode: "off", + gateCheckMode: "enabled", reviewCheckMode: "required", + linkedIssueGateMode: "block", // the default blocker mechanism for these tests: missing linked issue -> gate failure + ...settingsOverrides, + }); + // Without a registry snapshot the gate reports a "repo_unregistered" warning finding, which keeps the + // conclusion at "neutral" instead of "success"/"failure" -- register the repo so the tests below exercise + // real merge/close dispositions rather than the not-evaluated-yet state. + await persistRegistrySnapshot( + env, + normalizeRegistryPayload({ [REPO]: { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), + ); + } + + function prPayload(overrides: Record = {}): Record { + return { + action: "opened", + installation: { id: INSTALLATION_ID, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 60, + title: "A PR", + state: "open", + user: { login: "contributor" }, + head: { sha: "conv60" }, + labels: [], + body: "no linked issue here", // missing-linked-issue -> gate conclusion=failure under linkedIssueGateMode:block + mergeable_state: "clean", + reviewDecision: "APPROVED", + ...overrides, + }, + }; + } + + /** A fetch stub for one PR (number/head parametrized) with a controllable CI state, capturing whether a real + * merge (PUT .../pulls/N/merge) or close (PATCH .../pulls/N with state:"closed") mutation actually fired. */ + function stubPrFetch( + prNumber: number, + headSha: string, + seen: { closed: boolean; merged: boolean }, + ciState: "clear" | "pending" | "passed" = "clear", + ): void { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url === "https://api.github.com/graphql") { + return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([]); + if (url.includes(`/pulls/${prNumber}/reviews`)) return Response.json([]); + if (url.includes(`/pulls/${prNumber}/commits`)) return Response.json([]); + if (url.endsWith(`/pulls/${prNumber}/merge`) && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true }); + } + if (url.endsWith(`/pulls/${prNumber}`) && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")); + if (body.state === "closed") seen.closed = true; + return Response.json({ number: prNumber, state: body.state ?? "open" }); + } + if (url.endsWith(`/pulls/${prNumber}`)) { + return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: headSha }, mergeable_state: "clean" }); + } + if (url.includes(`/commits/${headSha}/check-runs`)) { + if (ciState === "pending") return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "in_progress", conclusion: null, app: { slug: "github-actions" } }] }); + if (ciState === "passed") return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + return Response.json({ total_count: 0, check_runs: [] }); + } + if (url.includes(`/commits/${headSha}/status`)) { + return Response.json({ state: ciState === "pending" ? "pending" : "success", statuses: [] }); + } + if (url.includes(`/issues/${prNumber}/labels`)) return Response.json([]); + if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); + return Response.json({}); + }); + } + + it("REGRESSION: a blocked contributor PR (plain gate failure) with close=auto is actually closed via the general heuristic-close path", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { autonomy: { close: "auto" } }); + const seen = { closed: false, merged: false }; + stubPrFetch(60, "conv60", seen); + resetMetrics(); + + await processJob(env, { type: "github-webhook", deliveryId: "conv-close", eventName: "pull_request", payload: prPayload() }); + + expect(seen.closed).toBe(true); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + // #terminal-outcome-audit: the disposition counter's "close" action_class, with the actual gate-blocker + // code (missing_linked_issue, from the default linkedIssueGateMode:block + no-linked-issue body) as the + // bounded blocker_class -- proof this reaches the real gate.blockers, not just a hardcoded label. + expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="close",autonomy_level="auto",blocker_class="missing_linked_issue",repo="redacted-1"} 1'); + const nativeDecision = await env.DB.prepare("select decision, summary, source from review_audit where event_type = 'gate_decision' and target_id = ?").bind(`${REPO}#60`).first<{ decision: string; summary: string; source: string }>(); + expect(nativeDecision).toMatchObject({ decision: "close", summary: "missing_linked_issue", source: "gittensory-native" }); + }); + + // REGRESSION (gate-flagged gap, #terminal-outcome-audit): a PR that touches a guardrail-protected path (e.g. + // .github/workflows/**) is otherwise clean, so the gate lands on conclusion:"neutral" via guardrailHit -- + // gate.blockers is empty for that conclusion (see evaluateGateCheckCore), so before this fix the disposition + // metric's blocker_class silently read "none", indistinguishable from a clean PR held on nothing more than + // pending CI. neutralHoldReasonCode recovers the real reason from gate.warnings instead. + it("a guardrail-path hold (neutral gate conclusion) records blocker_class=guardrail_hold, not 'none'", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { autonomy: { merge: "auto", close: "auto" }, linkedIssueGateMode: "off" }); + const seen = { closed: false, merged: false }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") return new Response("settings:\n hardGuardrailGlobs:\n - .github/workflows/**\n"); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/61/files")) return Response.json([{ filename: ".github/workflows/ci.yml", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+ x: 1" }]); + if (url.includes("/pulls/61/reviews")) return Response.json([]); + if (url.includes("/pulls/61/commits")) return Response.json([]); + if (url.endsWith("/pulls/61/merge") && method === "PUT") { seen.merged = true; return Response.json({ merged: true }); } + if (url.endsWith("/pulls/61") && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")); + if (body.state === "closed") seen.closed = true; + return Response.json({ number: 61, state: body.state ?? "open" }); + } + if (url.endsWith("/pulls/61")) return Response.json({ number: 61, state: "open", user: { login: "contributor" }, head: { sha: "conv61" }, mergeable_state: "clean" }); + if (url.includes("/commits/conv61/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/commits/conv61/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/61/labels")) return Response.json([]); + if (url.includes("/issues/61/comments")) return Response.json([]); + return Response.json({}); + }); + resetMetrics(); + + await processJob(env, { type: "github-webhook", deliveryId: "conv-guardrail", eventName: "pull_request", payload: prPayload({ number: 61, head: { sha: "conv61" }, body: "no linked issue needed" }) }); + + expect(seen.merged).toBe(false); + expect(seen.closed).toBe(false); + expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="hold",autonomy_level="auto",blocker_class="guardrail_hold",repo="redacted-1"} 1'); + const holdAudit = await env.DB.prepare("select metadata_json from audit_events where event_type = 'agent.action.hold' order by created_at desc limit 1").first<{ metadata_json: string }>(); + expect(JSON.parse(holdAudit?.metadata_json ?? "{}")).toMatchObject({ + repoFullName: REPO, + pullNumber: 61, + disposition: { actionClass: "hold", blockerClass: "guardrail_hold" }, + guardrailMatches: [{ path: ".github/workflows/ci.yml", glob: ".github/workflows/**" }], + }); + }); + + it("reviewCheckMode: disabled still auto-closes a blocked contributor PR via the general heuristic-close path (#2852)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { autonomy: { close: "auto" }, reviewCheckMode: "disabled" }); + const seen = { closed: false, merged: false }; + let checkRunApiCalls = 0; + stubPrFetch(66, "conv66", seen); + const realFetch = globalThis.fetch; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; + return realFetch(input, init); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-disabled-close", + eventName: "pull_request", + payload: prPayload({ number: 66, head: { sha: "conv66" } }), + }); + + expect(seen.closed).toBe(true); + expect(checkRunApiCalls).toBe(0); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBeGreaterThanOrEqual(1); + }); + + it("REGRESSION: a green-verdict PR with CI still pending is NOT merged (merge withheld until CI/mergeability settle)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off" }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + const seen = { closed: false, merged: false }; + stubPrFetch(61, "conv61", seen, "pending"); + + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-ci-pending", + eventName: "pull_request", + payload: prPayload({ number: 61, head: { sha: "conv61" }, body: "Closes #1" }), + }); + + expect(seen.merged).toBe(false); + const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); + expect(mergeAudit?.n).toBe(0); + }); + + it("REGRESSION (#selfhost-backlog-convergence): a CI-pending PR defers, then merges once check_suite.completed reports CI green (convergence chain)", async () => { + // maybeReReviewOnCiCompletion (processors.ts) gates its ENTIRE re-review loop on isConvergenceRepoAllowed + // (the GITTENSORY_REVIEW_REPOS cutover allowlist), independent of autonomy -- the check_suite/check_run + // "THE auto-merge trigger" path only fires for an allowlisted repo. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_REPOS: REPO }); + await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off" }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + const seen = { closed: false, merged: false }; + let ciState: "pending" | "passed" = "pending"; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + // Delegate to a fresh stub per call so the closure sees the CURRENT ciState -- stubPrFetch captures ciState + // by value at call time, so re-invoke its logic inline against the live ciState variable instead. + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url === "https://api.github.com/graphql") { + return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // A non-empty, non-guardrail file: an EMPTY files list is treated as "unresolved" and fails CLOSED into a + // guardrail hold (isGuardrailHit short-circuits true on changedPaths.length === 0) -- so this must return a + // real file for the merge disposition below to ever reach a genuine "success" gate conclusion. + if (url.includes("/pulls/62/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.includes("/pulls/62/reviews")) return Response.json([]); + if (url.includes("/pulls/62/commits")) return Response.json([]); + if (url.endsWith("/pulls/62/merge") && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true }); + } + if (url.endsWith("/pulls/62")) { + return Response.json({ number: 62, state: "open", user: { login: "contributor" }, head: { sha: "conv62" }, mergeable_state: "clean" }); + } + if (url.includes("/commits/conv62/check-runs")) { + return ciState === "pending" + ? Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "in_progress", conclusion: null, app: { slug: "github-actions" } }] }) + : Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + } + if (url.includes("/commits/conv62/status")) return Response.json({ state: ciState === "pending" ? "pending" : "success", statuses: [] }); + if (url.includes("/issues/62/labels")) return Response.json([]); + if (url.includes("/issues/62/comments")) return Response.json([]); + return Response.json({}); + }); + + // Step 1: a synchronize webhook while CI is still running -> merge withheld. + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-chain-1", + eventName: "pull_request", + payload: prPayload({ number: 62, head: { sha: "conv62" }, body: "Closes #1", action: "synchronize" }), + }); + expect(seen.merged).toBe(false); + + // Step 2: CI finishes; a check_suite.completed webhook for the SAME head re-triggers the pipeline, which now + // sees a passing CI aggregate and merges. + ciState = "passed"; + resetMetrics(); + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-chain-2", + eventName: "check_suite", + payload: { + action: "completed", + installation: { id: INSTALLATION_ID, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }, + check_suite: { head_sha: "conv62", conclusion: "success", pull_requests: [{ number: 62 }] }, + } as never, + }); + + expect(seen.merged).toBe(true); + const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); + expect(mergeAudit?.n).toBeGreaterThanOrEqual(1); + // #terminal-outcome-audit: the disposition counter's "merge" action_class, on the actual live call site. + expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="merge",autonomy_level="auto",blocker_class="none",repo="redacted-1"} 1'); + }); + + // #terminal-outcome-audit: end-to-end proof that the LIVE runAgentMaintenancePlanAndExecute call site (not just + // the extracted pure precisionBreakerDowngradeDirections/applyPrecisionBreakers unit tests) actually increments + // gittensory_precision_breaker_downgrades_total when an engaged accuracy circuit-breaker rewrites a real plan. + it("REGRESSION (#terminal-outcome-audit): an engaged holdonly breaker withholds a real would-merge AND increments the downgrade counter", async () => { + // Mirrors the "#selfhost-backlog-convergence" chain test above (same two-step CI-pending-then-green shape, + // the proven way this suite reaches a REAL merge attempt): a plain "opened" webhook with CI already green + // never reaches the merge decision in this harness; the check_suite.completed re-review path does. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_REPOS: REPO }); + await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off" }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + const seen = { closed: false, merged: false }; + let ciState: "pending" | "passed" = "pending"; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/access_tokens")) return Response.json({ token: "test-token" }); + if (url.includes("/pulls/65/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.includes("/pulls/65/reviews")) return Response.json([]); + if (url.includes("/pulls/65/commits")) return Response.json([]); + if (url.endsWith("/pulls/65/merge") && method === "PUT") { seen.merged = true; return Response.json({ merged: true }); } + if (url.endsWith("/pulls/65")) return Response.json({ number: 65, state: "open", user: { login: "contributor" }, head: { sha: "conv65" }, mergeable_state: "clean" }); + if (url.includes("/commits/conv65/check-runs")) { + return ciState === "pending" + ? Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "in_progress", conclusion: null, app: { slug: "github-actions" } }] }) + : Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + } + if (url.includes("/commits/conv65/status")) return Response.json({ state: ciState === "pending" ? "pending" : "success", statuses: [] }); + if (url.includes("/issues/65/labels")) return Response.json([]); + if (url.includes("/issues/65/comments")) return Response.json([]); + return Response.json({}); + }); + + // Step 1: a synchronize webhook while CI is still running — establishes the PR, no merge yet. + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-holdonly-1", + eventName: "pull_request", + payload: prPayload({ number: 65, head: { sha: "conv65" }, body: "Closes #1", action: "synchronize" }), + }); + expect(seen.merged).toBe(false); + + // Engage the merge-precision breaker for this exact repo BEFORE CI resolves — mirrors how runSelfTuneBreaker + // (or a human) would set it via system_flags ahead of the next re-review. + await env.DB.prepare("INSERT OR REPLACE INTO system_flags (key, value, updated_at) VALUES ('holdonly:JSONbored/gittensory', '1', CURRENT_TIMESTAMP)").run(); + resetMetrics(); + + // Step 2: CI finishes; a check_suite.completed webhook re-triggers the pipeline — without the breaker this + // would merge exactly like the sibling convergence-chain test above; the engaged breaker withholds it instead. + ciState = "passed"; + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-holdonly-2", + eventName: "check_suite", + payload: { + action: "completed", + installation: { id: INSTALLATION_ID, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }, + check_suite: { head_sha: "conv65", conclusion: "success", pull_requests: [{ number: 65 }] }, + } as never, + }); + + expect(seen.merged).toBe(false); + const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); + expect(mergeAudit?.n).toBe(0); + expect(await renderMetrics()).toContain('gittensory_precision_breaker_downgrades_total{direction="merge"} 1'); + // #terminal-outcome-audit: the ALWAYS-recorded disposition counter, placed before the "nothing was planned" + // early return -- this is the exact "hold, but no audit_events row at all" shape (the breaker downgrade + // leaves no merge/close action) that previously had zero aggregate signal. close autonomy is unset in this + // repo's settings (only merge/approve are configured), so it resolves to the default "observe". + expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="hold",autonomy_level="observe",blocker_class="none",repo="redacted-1"} 1'); + const holdAudit = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = 'agent.action.hold' order by created_at desc limit 1").first<{ detail: string; metadata_json: string }>(); + expect(holdAudit?.detail).toBe("auto-action held by precision circuit breaker"); + expect(JSON.parse(holdAudit?.metadata_json ?? "{}")).toMatchObject({ + repoFullName: REPO, + pullNumber: 65, + gateConclusion: "success", + ciState: "passed", + disposition: { actionClass: "hold", blockerClass: "none" }, + plannedActionClasses: ["merge"], + finalActionClasses: ["label"], + }); + }); + + it("reviewCheckMode: disabled still auto-merges a green PR via the general heuristic path, with ZERO check-run API calls (#2852)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off", reviewCheckMode: "disabled" }); + await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); + const seen = { closed: false, merged: false }; + let checkRunApiCalls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/64/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.includes("/pulls/64/reviews")) return Response.json([]); + if (url.includes("/pulls/64/commits")) return Response.json([]); + if (url.endsWith("/pulls/64/merge") && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true }); + } + if (url.endsWith("/pulls/64")) return Response.json({ number: 64, state: "open", user: { login: "contributor" }, head: { sha: "conv64" }, mergeable_state: "clean" }); + if (url.includes("/commits/conv64/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/commits/conv64/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/64/labels")) return Response.json([]); + if (url.includes("/issues/64/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-disabled-merge", + eventName: "pull_request", + payload: prPayload({ number: 64, head: { sha: "conv64" }, body: "Closes #1" }), + }); + + expect(seen.merged).toBe(true); + expect(checkRunApiCalls).toBe(0); + const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); + expect(mergeAudit?.n).toBeGreaterThanOrEqual(1); + }); + + it("reviewCheckMode: disabled still auto-merges an AUTHOR-LESS (ghost) PR when autonomy is configured (#2852)", async () => { + // A ghost PR (no `user` at all -> authorLogin null) is the one other early-return in + // maybePublishPrPublicSurface gated on gateEnabled (`if (!author && !gateEnabled && !autonomyNeedsGateEvaluation) + // return undefined;`) -- proves autonomyNeedsGateEvaluation also keeps THIS guard from bailing. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off", reviewCheckMode: "disabled" }); + const seen = { closed: false, merged: false }; + let checkRunApiCalls = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/67/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.includes("/pulls/67/reviews")) return Response.json([]); + if (url.includes("/pulls/67/commits")) return Response.json([]); + if (url.endsWith("/pulls/67/merge") && method === "PUT") { + seen.merged = true; + return Response.json({ merged: true }); + } + if (url.endsWith("/pulls/67")) return Response.json({ number: 67, state: "open", head: { sha: "conv67" }, mergeable_state: "clean" }); + if (url.includes("/commits/conv67/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/commits/conv67/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/67/labels")) return Response.json([]); + if (url.includes("/issues/67/comments")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-disabled-ghost-author", + eventName: "pull_request", + payload: prPayload({ number: 67, head: { sha: "conv67" }, body: "Closes #1", user: undefined }), + }); + + expect(seen.merged).toBe(true); + expect(checkRunApiCalls).toBe(0); + }); + + it("an author-less (ghost) PR with the check-run disabled and NO autonomy configured stays fully silent (early-return preserved)", async () => { + // Mirrors the ghost-PR test above but WITHOUT autonomy configured -- proves the early return in + // maybePublishPrPublicSurface still fires (bails to undefined, no work at all) when neither gateEnabled nor + // autonomyNeedsGateEvaluation applies, exactly as before #2852. + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { reviewCheckMode: "disabled" }); // autonomy defaults to {} (unconfigured) + let checkRunApiCalls = 0; + let mergeAttempted = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; + if (url.endsWith("/pulls/68/merge")) mergeAttempted = true; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "ghost-no-autonomy", + eventName: "pull_request", + payload: prPayload({ number: 68, head: { sha: "conv68" }, body: "no linked issue here", user: undefined }), + }); + + expect(checkRunApiCalls).toBe(0); + expect(mergeAttempted).toBe(false); + }); + + it("reviewCheckMode: disabled still posts the sticky PR comment and label (public surface is independent of the check-run publish decision) (#2852)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { + autonomy: { merge: "auto", approve: "auto" }, + linkedIssueGateMode: "off", + reviewCheckMode: "disabled", + commentMode: "all_prs", + publicSurface: "comment_and_label", + }); + let checkRunApiCalls = 0; + let commentPosted = false; + let labelApplied = false; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; + if (url === "https://api.gittensor.io/miners") return Response.json([]); + if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/pulls/65/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); + if (url.includes("/pulls/65/reviews")) return Response.json([]); + if (url.includes("/pulls/65/commits")) return Response.json([]); + if (url.endsWith("/pulls/65")) return Response.json({ number: 65, state: "open", user: { login: "contributor" }, head: { sha: "conv65" }, mergeable_state: "clean" }); + if (url.includes("/commits/conv65/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); + if (url.includes("/commits/conv65/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/65/comments") && method === "POST") { + commentPosted = true; + return Response.json({ id: 1 }); + } + if (url.includes("/issues/65/labels") && method === "POST") { + labelApplied = true; + return Response.json([]); + } + if (url.includes("/issues/65/comments") || url.includes("/issues/65/labels")) return Response.json([]); + return Response.json({}); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-disabled-surface", + eventName: "pull_request", + payload: prPayload({ number: 65, head: { sha: "conv65" }, body: "Closes #1" }), + }); + + expect(checkRunApiCalls).toBe(0); + expect(commentPosted).toBe(true); + expect(labelApplied).toBe(true); + }); + + it("REGRESSION: closeOwnerAuthors=false (default) protects an owner-authored blocked PR from the general heuristic-close path", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { autonomy: { close: "auto" } }); // closeOwnerAuthors defaults false + const seen = { closed: false, merged: false }; + stubPrFetch(63, "conv63", seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-owner-protected", + eventName: "pull_request", + payload: prPayload({ number: 63, head: { sha: "conv63" }, user: { login: "JSONbored" } }), // author = repo owner + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBe(0); + // Enriched hold-audit fields (#selfhost-holdplan-audit): this scenario's gate blocker (missing linked issue) + // already produced a specific "protected author" detail before this change -- what's new here is that + // `metadata` now ALSO carries the structured closeEligible/closeAutonomy/protectedAuthor fields, so a hold + // is debuggable from the audit table alone. The actual bug fix -- a RED-CI hold (no gate blocker at all) + // gaining the same protected-author/close-autonomy disambiguation the gate-blocker branch already had -- + // is unit-tested directly against agentHoldAuditDetail in precision-breakers-chain.test.ts, where the two + // branches can be exercised independently without needing a webhook fixture that produces CI-failed with + // zero gate blockers. + const holdAudit = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = 'agent.action.hold' order by created_at desc limit 1").first<{ detail: string; metadata_json: string }>(); + expect(holdAudit?.detail).toBe("close withheld for protected author on gate blocker missing_linked_issue"); + expect(JSON.parse(holdAudit?.metadata_json ?? "{}")).toMatchObject({ + repoFullName: "JSONbored/gittensory", + pullNumber: 63, + closeEligible: false, + closeAutonomy: "auto", + // The repo owner is also treated as an admin (GitHub's own collaborator-permission model), so both flags + // are true for this fixture -- only `automation` is meaningfully independent of `owner` here. + protectedAuthor: { owner: true, admin: true, automation: false }, + closeOwnerAuthors: false, + }); + }); + + it("REGRESSION: closeOwnerAuthors=true allows the general heuristic-close path to close a blocked owner-authored PR", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await setupAutoActionRepo(env, { autonomy: { close: "auto" }, closeOwnerAuthors: true }); + const seen = { closed: false, merged: false }; + stubPrFetch(64, "conv64", seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-owner-allowed", + eventName: "pull_request", + payload: prPayload({ number: 64, head: { sha: "conv64" }, user: { login: "JSONbored" } }), + }); + + expect(seen.closed).toBe(true); + }); + + it("REGRESSION (#2133): an ADMIN_GITHUB_LOGINS fleet-operator author is exempt from the general heuristic-close path, same as the literal repo owner", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), ADMIN_GITHUB_LOGINS: "admin-user" }); + await setupAutoActionRepo(env, { autonomy: { close: "auto" } }); // closeOwnerAuthors defaults false + const seen = { closed: false, merged: false }; + stubPrFetch(65, "conv65", seen); + + await processJob(env, { + type: "github-webhook", + deliveryId: "conv-admin-protected", + eventName: "pull_request", + payload: prPayload({ number: 65, head: { sha: "conv65" }, user: { login: "admin-user" } }), + }); + + expect(seen.closed).toBe(false); + const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); + expect(closeAudit?.n).toBe(0); + }); +}); + +// #automation-bot-skip: waste elimination for known automation authors (release-please's github-actions[bot], +// Renovate, Dependabot). End-to-end wiring on top of automation-bot-skip.test.ts's pure-function coverage -- +// these pin the webhook + re-entry integration points, including the SECURITY property that a human pushing +// to an existing bot PR's branch still gets full review of their own commits. +describe("automation-bot-skip: end-to-end webhook + re-entry wiring (#automation-bot-skip)", () => { + const basePayload = { + installation: { id: 9101, account: { login: "owner", id: 1, type: "Organization" } }, + repository: { name: "bot-skip-repo", full_name: "owner/bot-skip-repo", private: false, owner: { login: "owner" } }, + }; + + // resolveRepositorySettings itself probes for a config-as-code override (.gittensory.yml/.json in both the + // repo root and .github/) BEFORE the skip check can even run (it needs the resolved settings for the + // per-repo override) -- so those 4 raw.githubusercontent.com probes are unavoidable, pre-existing overhead + // on EVERY webhook, not the "waste" this feature eliminates. The real signal is that NOTHING beyond that + // touches the actual GitHub REST API (api.github.com) -- no installation-token fetch, no PR/files read, no + // comment/check-run publish, no AI provider call. + async function fetchCallTracker() { + const state = { urls: [] as string[] }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + state.urls.push(input.toString()); + return new Response("not found", { status: 404 }); + }); + return state; + } + + it("a genuine bot-triggered PR (sender IS the bot, matching the stored author) is skipped entirely: audited, zero GitHub/AI fetch calls, delivery marked processed", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + const calls = await fetchCallTracker(); + + await processJob(env, { + type: "github-webhook", + deliveryId: "bot-skip-genuine", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + sender: { login: "renovate[bot]", type: "Bot" }, + pull_request: { number: 401, title: "chore(deps): bump foo", state: "open", user: { login: "renovate[bot]", type: "Bot" }, labels: [], body: "" }, + }, + }); + + expect(calls.urls.some((url) => url.includes("api.github.com"))).toBe(false); + const skipAudit = await env.DB.prepare("select detail, actor from audit_events where event_type = 'github_app.automation_bot_pr_skipped' and target_key = 'owner/bot-skip-repo#401'").first<{ detail: string; actor: string }>(); + expect(skipAudit?.actor).toBe("renovate[bot]"); + expect(skipAudit?.detail).toContain("automation-bot author"); + const webhookEvent = await env.DB.prepare("select status from webhook_events where delivery_id = 'bot-skip-genuine'").first<{ status: string }>(); + expect(webhookEvent?.status).toBe("processed"); + }); + + it("SECURITY: a human who pushes to an existing bot-authored PR's branch (synchronize) is NOT skipped -- the live webhook actor, not the stored author, gates the skip", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await fetchCallTracker(); + + await processJob(env, { + type: "github-webhook", + deliveryId: "bot-skip-exploit-attempt", + eventName: "pull_request", + payload: { + action: "synchronize", + ...basePayload, + sender: { login: "malicious-contributor", type: "User" }, + pull_request: { number: 402, title: "chore(deps): bump foo", state: "open", user: { login: "renovate[bot]", type: "Bot" }, labels: [], body: "", head: { sha: "hijacked-sha" } }, + }, + }); + + const skipAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.automation_bot_pr_skipped' and target_key = 'owner/bot-skip-repo#402'").first<{ n: number }>(); + expect(skipAudit?.n).toBe(0); + }); + + it("a per-repo 'off' override forces full review even for a genuine bot-triggered PR", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await fetchCallTracker(); + await upsertRepositorySettings(env, { repoFullName: "owner/bot-skip-repo", skipAutomationBotAuthors: "off" }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "bot-skip-repo-off-override", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + sender: { login: "dependabot[bot]", type: "Bot" }, + pull_request: { number: 403, title: "chore(deps): bump bar", state: "open", user: { login: "dependabot[bot]", type: "Bot" }, labels: [], body: "" }, + }, + }); + + const skipAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.automation_bot_pr_skipped' and target_key = 'owner/bot-skip-repo#403'").first<{ n: number }>(); + expect(skipAudit?.n).toBe(0); + }); + + it("a per-repo 'enabled' override skips a genuine bot-triggered PR even when the global default is OFF", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_SKIP_AUTOMATION_BOT_PRS: "false" }); + const calls = await fetchCallTracker(); + await upsertRepositorySettings(env, { repoFullName: "owner/bot-skip-repo", skipAutomationBotAuthors: "enabled" }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "bot-skip-repo-enabled-override", + eventName: "pull_request", + payload: { + action: "opened", + ...basePayload, + sender: { login: "github-actions[bot]", type: "Bot" }, + pull_request: { number: 404, title: "chore(release): 1.2.3", state: "open", user: { login: "github-actions[bot]", type: "Bot" }, labels: [], body: "" }, + }, + }); + + expect(calls.urls.some((url) => url.includes("api.github.com"))).toBe(false); + const skipAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.automation_bot_pr_skipped' and target_key = 'owner/bot-skip-repo#404'").first<{ n: number }>(); + expect(skipAudit?.n).toBe(1); + }); + + it("the re-entry sweep path (agent-regate-pr) also respects the skip for a stored bot author, without even the live resync fetch", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertInstallation(env, { action: "created", installation: { id: 9101, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); + await upsertRepositoryFromGitHub(env, { name: "bot-skip-repo", full_name: "owner/bot-skip-repo", private: false, owner: { login: "owner" } }, 9101); + await upsertPullRequestFromGitHub(env, "owner/bot-skip-repo", { number: 405, title: "chore(deps): bump baz", state: "open", user: { login: "renovate[bot]", type: "Bot" }, head: { sha: "sha405" }, labels: [], body: "" }); + const calls = await fetchCallTracker(); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "bot-skip-sweep", repoFullName: "owner/bot-skip-repo", prNumber: 405, installationId: 9101 }); + + // The re-entry check runs BEFORE even the live-head resync GET, so a genuine bot author skips without any + // GitHub REST API call at all -- not merely without a comment/check-run publish. + expect(calls.urls.some((url) => url.includes("api.github.com"))).toBe(false); + const stored = await getPullRequest(env, "owner/bot-skip-repo", 405); + expect(stored?.headSha).toBe("sha405"); + }); +}); + diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index d50dd73dad..1c7265cb7c 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -104,6 +104,118 @@ async function sweepAndDrainPerPr(env: Env, repoFullName: string): Promise[1]; + +function commandAnswer(id: string, command: string, overrides: Partial = {}): CommandAnswerFixture { + return { + id, + repoFullName: "JSONbored/gittensory", + issueNumber: 77, + command, + requestCommentId: 7, + responseCommentId: 9001, + responseUrl: "https://github.com/JSONbored/gittensory/pull/77#issuecomment-9001", + actorKind: "maintainer" as const, + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + metadata: {}, + ...overrides, + }; +} + +function commandAnswerBody(answerId: string, command: string): string { + return [ + "", + ``, + `Command: \`@gittensory ${command}\``, + "Feedback is aggregate-only.", + ].join("\n"); +} + +function queueMinerSnapshot(login: string) { + return { + source: "gittensor_api" as const, + githubId: "123", + githubUsername: login, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + issueDiscoveryScore: 0, + issueTokenScore: 0, + issueCredibility: 1, + isIssueEligible: false, + issueEligibleRepoCount: 0, + alphaPerDay: 0, + taoPerDay: 0, + usdPerDay: 0, + totals: { + pullRequests: 3, + mergedPullRequests: 2, + openPullRequests: 1, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + }, + repositories: [], + pullRequests: [], + issueLabels: [], + }; +} + +function b64(value: string): string { + return Buffer.from(value, "utf8").toString("base64"); +} + +function withProductUsageInsertFailure(env: Env): Env { + const db = env.DB as unknown as { prepare(sql: string): unknown; batch(statements: unknown[]): Promise }; + return { + ...env, + DB: { + prepare(sql: string) { + if (sql.includes("product_usage_events")) throw new Error("product usage insert failed"); + return db.prepare.call(db, sql); + }, + batch(statements: unknown[]) { + return db.batch.call(db, statements); + }, + } as unknown as D1Database, + }; +} + +async function generatePrivateKeyPem(): Promise { + const key = (await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); + const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; +} + describe("queue processors", () => { // Freshness-SLO fixtures are dated relative to late May 2026; pin the clock so staleness windows // stay deterministic regardless of when CI runs. @@ -128,6 +240,7 @@ describe("queue processors", () => { vi.restoreAllMocks(); }); + it("fans build-contributor-evidence out into per-batch jobs when the login set exceeds CONTRIBUTOR_EVIDENCE_BATCH_SIZE (#1941)", async () => { vi.stubEnv("CONTRIBUTOR_EVIDENCE_BATCH_SIZE", "1"); // force a fan-out at > 1 derived login const env = createTestEnv(); @@ -6914,27245 +7027,4 @@ describe("queue processors", () => { // Shared cache-input-fingerprint builder for the #4603 pair below -- mirrors "#1"'s own inline fingerprint, // parameterized only by PR title/number/sha so both tests get a genuine cache HIT (aiCalls stays 0) instead of // silently falling through to a real (unmocked-defect) AI call on a fingerprint mismatch. - async function cachedSubFloorDefectFingerprint(title: string): Promise { - return aiReviewCacheInputFingerprint({ - title, - mode: "block", - byok: false, - provider: null, - model: null, - aiReviewAllAuthors: false, - aiReviewCloseConfidence: undefined, - aiReviewCombine: null, - aiReviewOnMerge: null, - aiReviewReviewers: null, - gatePack: "oss-anti-slop", - reviewerPlan: undefined, - selfHostProviderConfig: null, - selfHostAiModelOverride: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null, ollamaModel: null, openaiModel: null, openaiCompatibleModel: null, anthropicModel: null }, - reviewFiles: [{ path: "src/a.ts", status: "modified", patch: "@@\n+export const ok = value.length;", additions: 1, deletions: 0 }], - profile: null, - securityFocus: false, - inlineComments: false, - pathInstructions: [], - pathGuidance: "", - repoInstructions: null, - excludePaths: [], - pathFilters: [], - changedPaths: ["src/a.ts"], - features: { grounding: false, rag: false, enrichment: false, reputation: false, cultureProfile: false, impactMap: false }, - }); - } - - it("#4603: a sub-floor cached ai_consensus_defect under hold_for_review (default) still fails the gate but does NOT one-shot-close", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); - // aiReviewLowConfidenceDisposition left UNSET — the shipped default (hold_for_review) is what's under test. - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { close: "auto" }, aiReviewMode: "block", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 8, title: "Sub-floor defect PR", state: "open", user: { login: "contributor" }, head: { sha: "b8" }, labels: [], body: "Closes #1" }); - await upsertPullRequestFile(env, { repoFullName: "owner/agent-repo", pullNumber: 8, path: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, payload: { patch: "@@\n+export const ok = value.length;" } }); - const inputFingerprint = await cachedSubFloorDefectFingerprint("Sub-floor defect PR"); - await putCachedAiReview(env, "owner/agent-repo", 8, "b8", "block", { - notes: "cached review", - reviewerCount: 2, - // 0.3 is well below the default 0.93 close-confidence floor. - findings: [{ code: "ai_consensus_defect", severity: "critical", title: "Cached defect", detail: "Cached critical defect.", confidence: 0.3 }], - metadata: { inputFingerprint }, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/8/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = value.length;" }]); - if (url.endsWith("/pulls/8") && init?.method === "PATCH") return Response.json({ number: 8, state: "closed" }); - if (url.endsWith("/pulls/8")) return Response.json({ number: 8, title: "Sub-floor defect PR", state: "open", user: { login: "contributor" }, head: { sha: "b8" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/b8/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/b8/status")) return Response.json({ state: "success", statuses: [] }); - if (url.endsWith("/pulls/8/reviews") && init?.method === "POST") return Response.json({ id: 1 }); - if (url.endsWith("/pulls/8/reviews")) return Response.json([]); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await sweepAndDrainPerPr(env, "owner/agent-repo"); - - expect(aiCalls).toBe(0); // the cached AI review was reused — the LLM was never called for this head SHA - // The gate still failed on the AI-judgment blocker (the merge stays blocked). - const blocker = await env.DB.prepare("select blocker_codes_json from gate_outcomes where repo_full_name = ? and pull_number = ? order by rowid desc limit 1").bind("owner/agent-repo", 8).first<{ blocker_codes_json: string }>(); - expect(blocker?.blocker_codes_json).toContain("ai_consensus_defect"); - // But it was NOT one-shot-closed -- the hold suppressed the close autonomy would otherwise have taken. - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and detail like ?").bind("agent.action.close", "%closed%").first<{ n: number }>(); - expect(closeAudit?.n).toBe(0); - const pr8 = await getPullRequest(env, "owner/agent-repo", 8); - expect(pr8?.state).toBe("open"); - }); - - it("#4603: the SAME sub-floor defect one-shot-closes when aiReviewLowConfidenceDisposition is explicitly one_shot", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { pull_requests: "write" }, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { close: "auto" }, aiReviewMode: "block", aiReviewLowConfidenceDisposition: "one_shot", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 9, title: "Sub-floor defect PR (one_shot)", state: "open", user: { login: "contributor" }, head: { sha: "c9" }, labels: [], body: "Closes #1" }); - await upsertPullRequestFile(env, { repoFullName: "owner/agent-repo", pullNumber: 9, path: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, payload: { patch: "@@\n+export const ok = value.length;" } }); - const inputFingerprint = await cachedSubFloorDefectFingerprint("Sub-floor defect PR (one_shot)"); - await putCachedAiReview(env, "owner/agent-repo", 9, "c9", "block", { - notes: "cached review", - reviewerCount: 2, - findings: [{ code: "ai_consensus_defect", severity: "critical", title: "Cached defect", detail: "Cached critical defect.", confidence: 0.3 }], - metadata: { inputFingerprint }, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/9/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = value.length;" }]); - if (url.endsWith("/pulls/9") && init?.method === "PATCH") return Response.json({ number: 9, state: "closed" }); - if (url.endsWith("/pulls/9")) return Response.json({ number: 9, title: "Sub-floor defect PR (one_shot)", state: "open", user: { login: "contributor" }, head: { sha: "c9" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/c9/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/c9/status")) return Response.json({ state: "success", statuses: [] }); - if (url.endsWith("/pulls/9/reviews") && init?.method === "POST") return Response.json({ id: 1 }); - if (url.endsWith("/pulls/9/reviews")) return Response.json([]); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await sweepAndDrainPerPr(env, "owner/agent-repo"); - - expect(aiCalls).toBe(0); - const blocker = await env.DB.prepare("select blocker_codes_json from gate_outcomes where repo_full_name = ? and pull_number = ? order by rowid desc limit 1").bind("owner/agent-repo", 9).first<{ blocker_codes_json: string }>(); - expect(blocker?.blocker_codes_json).toContain("ai_consensus_defect"); - // one_shot ignores the floor: the close autonomy actually fires this time (contrast with the hold_for_review - // test above, whose closeAudit count is 0). The PR row's `state` column only flips once GitHub's own - // `closed` webhook round-trips back through the normal sync path -- a separate delivery this sweep-driven - // test does not simulate (see the identical gap documented at this file's #linked-issue-hard-rule-persistence - // two-pass test), so the disposition planner's own audit record is the observable proof instead. - const close = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1").bind("agent.action.close").first<{ outcome: string; detail: string }>(); - expect(close?.outcome).toBe("completed"); - }); - - it("posts the 🟪 reviewing placeholder before the AI review runs, then overwrites it with the verdict (#reviewing-placeholder)", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => ({ response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }), - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - const stickyComment: { current: { id: number; body: string } | null } = { current: null }; - let firstWriteWasPlaceholder = false; - let postCount = 0; - let patchCount = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/7/comments") && method === "GET") { - return Response.json(stickyComment.current ? [{ ...stickyComment.current, user: { login: "gittensory[bot]", type: "Bot" } }] : []); - } - if (url.includes("/issues/7/comments") && method === "POST") { - const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - postCount += 1; - if (postCount === 1) firstWriteWasPlaceholder = body.includes("is reviewing"); - stickyComment.current = { id: 1, body }; - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes("/issues/comments/1") && method === "PATCH") { - const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - patchCount += 1; - stickyComment.current = { id: 1, body }; - return Response.json({ id: 1 }, { status: 200 }); - } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "reviewing-placeholder", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, - }, - }); - - // The transient purple placeholder is the first write, then the final verdict updates the same sticky comment. - expect(postCount).toBe(1); - expect(patchCount).toBeGreaterThanOrEqual(1); - expect(firstWriteWasPlaceholder).toBe(true); - expect(stickyComment.current?.body).toContain(PR_PANEL_COMMENT_MARKER); - expect(stickyComment.current?.body).toContain("Thanks for the contribution"); - expect(stickyComment.current?.body).not.toContain("is reviewing"); - }); - - it("flags an open-PR file-path collision against a sibling PR when GITTENSORY_OPEN_PR_FILE_COLLISION is on (#2653)", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_OPEN_PR_FILE_COLLISION: "true", - }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - aiReviewMode: "off", - }); - // A sibling PR (different author, unrelated title) already open and already detail-synced — its files are - // in the pull_request_files cache, the same way routine backfill would have populated them. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 8, - title: "Document logging output", - state: "open", - user: { login: "other-author" }, - head: { sha: "b8" }, - labels: [], - body: "", - }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 8, path: "src/shared/util.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); - // The PR under review (#7) was ALSO already detail-synced against the same file before this rerun. - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 7, path: "src/shared/util.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); - const stickyComment: { current: { id: number; body: string } | null } = { current: null }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([{ uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/shared/util.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Improve widget rendering", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") { - const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - stickyComment.current = { id: 1, body }; - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "open-pr-file-collision", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Improve widget rendering", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }, - }, - }); - - // The sibling PR #8 (different author, same file, unrelated title) surfaces in the related-work panel — - // proof the enriched changedFiles flowed through buildCollisionReport's existing termOverlap scoring. - expect(stickyComment.current?.body).toContain("#8"); - }); - - it("does NOT flag an open-PR file-path collision when GITTENSORY_OPEN_PR_FILE_COLLISION is unset (byte-identical default)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - aiReviewMode: "off", - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 8, - title: "Document logging output", - state: "open", - user: { login: "other-author" }, - head: { sha: "b8" }, - labels: [], - body: "", - }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 8, path: "src/shared/util.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 7, path: "src/shared/util.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); - const stickyComment: { current: { id: number; body: string } | null } = { current: null }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([{ uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/shared/util.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Improve widget rendering", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") { - const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - stickyComment.current = { id: 1, body }; - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "open-pr-file-collision-flag-off", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Improve widget rendering", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }, - }, - }); - - expect(stickyComment.current?.body).not.toContain("#8"); - }); - - it("computes the AI review cache fingerprint with a self-host reviewer plan and converged grounding/enrichment on (#2119)", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => { - aiCalls += 1; - return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; - }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - // A self-host reviewer plan (not just BYOK/cloud provider/model) plus its underlying provider config. - AI_REVIEW_PLAN: { reviewers: [{ model: "claude-code" }], combine: "single" } as never, - CLAUDE_AI_MODEL: "sonnet", - CLAUDE_AI_EFFORT: "high", - // Grounding + enrichment ON, with the repo allowlisted for convergence, so both feature flags - // resolve past their `isXEnabled(env) && convergedRepoAllowed` check into the fingerprint. - GITTENSORY_REVIEW_GROUNDING: "true", - GITTENSORY_REVIEW_ENRICHMENT: "true", - REES_URL: "https://rees.example", - GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) - return Response.json([ - { filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }, - // GitHub omits `patch` for binary/oversized files -- the fingerprint must still normalize this case. - { filename: "assets/logo.png", status: "modified", additions: 0, deletions: 0, changes: 0 }, - ]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - // REES enrichment + any other unmatched call degrade fail-open on a generic empty response. - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "self-host-plan-converged-features", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, - }, - }); - - // The review ran fresh (no pre-seeded cache to reuse), reaching the fingerprint computation with the - // self-host reviewer plan, its provider config, and both converged feature checks evaluated. - expect(aiCalls).toBeGreaterThan(0); - }); - - it("computes the AI review cache fingerprint with the repo quality-culture profile on, both the global flag and the per-repo opt-in (#2995)", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => { - aiCalls += 1; - return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; - }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - // Both gates on: the global capability switch, and — unlike grounding/enrichment/RAG/reputation, which are - // env-only — the per-repo `.gittensory.yml` opt-in mocked below, so `dynamicReviewFeatures.cultureProfile` - // (src/queue/processors.ts) actually evaluates its `&&` right-hand side true, not just short-circuits. - GITTENSORY_REVIEW_CULTURE_PROFILE: "true", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - // The repo's own review.culture_profile opt-in. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n culture_profile: true\n"); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "culture-profile-converged-feature", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, - }, - }); - - // The review ran fresh, reaching the fingerprint computation with the culture-profile feature evaluated — - // this repo has no merge history seeded, so the context itself is empty, but the FLAG combination (not the - // context content) is what dynamicReviewFeatures.cultureProfile tracks for cache-bypass purposes. - expect(aiCalls).toBeGreaterThan(0); - }); - - it("marks a cached AI review non-durable (cacheable=0) when the impact-map feature is on, even with grounding/rag/enrichment/reputation all off (#2182-#2186)", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => { - aiCalls += 1; - return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; - }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - // Both gates on: the global capability switch, and (like culture-profile above, unlike - // grounding/enrichment/RAG/reputation which are env-only) the per-repo `.gittensory.yml` opt-in mocked - // below, so `dynamicReviewFeatures.impactMap` (src/queue/processors.ts) actually evaluates - // shouldComputeImpactMap's `&&` right-hand side true, not just short-circuits. - GITTENSORY_REVIEW_IMPACT_MAP: "true", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - // The repo's own review.impact_map opt-in. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n impact_map: true\n"); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "impact-map-non-durable", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, - }, - }); - - expect(aiCalls).toBeGreaterThan(0); - const cached = await env.DB.prepare("select cacheable from ai_review_cache where repo_full_name = ? and pull_number = ? and head_sha = ?") - .bind("JSONbored/gittensory", 7, "a7") - .first<{ cacheable: number }>(); - // Never durably cacheable on its own merits, even though grounding/rag/enrichment/reputation are all off in - // this env -- impact-map alone is enough to trip dynamicReviewContextActive. - expect(cached?.cacheable).toBe(0); - }); - - it("reuses a dynamic-context (grounding) AI review indefinitely once published, even long past the old cooldown window (#2119, #regate-churn)", async () => { - // Grounding/RAG/enrichment/reputation each pull TIME-VARYING external context (live CI checks, the vector - // index, REES/CVE data, reputation) that can change for the SAME head SHA without the feature flags - // themselves flipping — so treating a hit here as an INDEFINITELY durable result BEFORE it is ever published - // could replay a review built against now-stale context forever. #regate-churn (root-caused in production: a - // single dynamic-context PR generated 259 of 281 AI review calls in 24h at an unchanged head, because this - // used to re-run UNCONDITIONALLY on every single call, with no bound at all) FIRST changed this to a bounded, - // non-durable reuse (AI_REVIEW_NON_CACHEABLE_RETRY_COOLDOWN_MS) — but that bound was itself still an - // UNBOUNDED total spend over the PR's lifetime (one fresh call every cooldown window, forever). Once the - // review has actually been PUBLISHED to the PR, `published_at` makes it authoritative for its exact - // head+fingerprint regardless of how much time elapses — only a real content/config change or an explicit - // maintainer force-rerun may spend another one. - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => { - aiCalls += 1; - return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; - }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - GITTENSORY_REVIEW_GROUNDING: "true", - GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - const webhook = { - type: "github-webhook" as const, - eventName: "pull_request" as const, - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" as const } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, - }, - }; - await processJob(env, { ...webhook, deliveryId: "dynamic-context-bypass-1" }); - const firstRunAiCalls = aiCalls; - expect(firstRunAiCalls).toBeGreaterThan(0); - const cached = await env.DB.prepare("select cacheable, published_at as publishedAt from ai_review_cache where repo_full_name = ? and pull_number = ? and head_sha = ?") - .bind("JSONbored/gittensory", 7, "a7") - .first<{ cacheable: number; publishedAt: string | null }>(); - expect(cached?.cacheable).toBe(0); // never durably cacheable on its own merits - expect(cached?.publishedAt).not.toBeNull(); // but it WAS published to the PR this pass - - // Re-review of the SAME head with the SAME (unchanged) inputs, shortly after: reused, no additional LLM spend. - vi.setSystemTime(new Date("2026-05-28T00:05:00.000Z")); - await processJob(env, { ...webhook, deliveryId: "dynamic-context-bypass-2" }); - expect(aiCalls).toBe(firstRunAiCalls); - - // What used to be the cooldown window (30 min) elapses, then a full day, then a full month — the published - // snapshot is authoritative regardless: none of these buy a fresh call. - for (const later of ["2026-05-28T00:31:00.000Z", "2026-05-29T00:00:00.000Z", "2026-06-28T00:00:00.000Z"]) { - vi.setSystemTime(new Date(later)); - await processJob(env, { ...webhook, deliveryId: `dynamic-context-bypass-later-${later}` }); - } - expect(aiCalls).toBe(firstRunAiCalls); - }); - - it("continues to final verdict when the reviewing placeholder audit write fails", async () => { - const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; - const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { - if (event.eventType === "github_app.reviewing_placeholder_failed") - throw new Error("D1 audit failed"); - await originalRecordAuditEvent(auditEnv, event); - }); - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => ({ response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }), - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - const postedBodies: string[] = []; - let postAttempts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/47/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/47")) return Response.json({ number: 47, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a47" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a47/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a47/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/47/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/47/comments") && method === "POST") { - postAttempts += 1; - const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - if (postAttempts === 1) return new Response(JSON.stringify({ message: "temporary comment failure" }), { status: 500 }); - postedBodies.push(body); - return Response.json({ id: 47 }, { status: 201 }); - } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "reviewing-placeholder-audit-fails", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 47, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a47" }, labels: [], body: "Closes #1" }, - }, - }); - - expect(postAttempts).toBeGreaterThanOrEqual(2); - expect(postedBodies.some((body) => !body.includes("is reviewing"))).toBe(true); - expect(auditSpy).toHaveBeenCalledWith( - env, - expect.objectContaining({ eventType: "github_app.reviewing_placeholder_failed" }), - ); - auditSpy.mockRestore(); - }); - - it("posts the 🟪 reviewing placeholder for non-AI comment refreshes, then overwrites it with the verdict", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "false", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot(env, normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z")); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "advisory" }); - const stickyComment: { current: { id: number; body: string } | null } = { current: null }; - let postCount = 0; - let patchCount = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/8/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/8")) return Response.json({ number: 8, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a8/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a8/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/8/comments") && method === "GET") { - return Response.json(stickyComment.current ? [{ ...stickyComment.current, user: { login: "gittensory[bot]", type: "Bot" } }] : []); - } - if (url.includes("/issues/8/comments") && method === "POST") { - const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - postCount += 1; - stickyComment.current = { id: 1, body }; - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes("/issues/comments/1") && method === "PATCH") { - const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - patchCount += 1; - stickyComment.current = { id: 1, body }; - return Response.json({ id: 1 }, { status: 200 }); - } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "reviewing-placeholder-disabled-ai", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 8, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }, - }, - }); - - expect(aiCalls).toBe(0); - expect(postCount).toBe(1); - expect(patchCount).toBeGreaterThanOrEqual(1); - expect(stickyComment.current?.body).toContain(PR_PANEL_COMMENT_MARKER); - expect(stickyComment.current?.body).toContain("Thanks for the contribution"); - expect(stickyComment.current?.body).not.toContain("is reviewing"); - }); - - it("keeps the PR comment in 🟪 reviewing state and retries when the final comment update is rate-limited", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "false", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - aiReviewMode: "advisory", - }); - const postedBodies: string[] = []; - let finalCommentAttempted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/9/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/9")) return Response.json({ number: 9, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a9/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a9/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/9/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/9/comments") && method === "POST") { - const body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - if (postedBodies.length === 0) { - postedBodies.push(body); - return Response.json({ id: 1 }, { status: 201 }); - } - finalCommentAttempted = true; - return new Response(JSON.stringify({ message: "API rate limit exceeded" }), { - status: 403, - headers: { "x-ratelimit-remaining": "0" }, - }); - } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "reviewing-placeholder-comment-ratelimit", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 9, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "Closes #1" }, - }, - }), - ).rejects.toThrow(/rate limit/i); - - expect(finalCommentAttempted).toBe(true); - expect(postedBodies).toHaveLength(1); - expect(postedBodies[0]).toContain("is reviewing"); - expect(postedBodies[0]).toContain("🟪"); - }); - - it("publishes AI notes when the review omits a narrative assessment", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => { - aiCalls += 1; - return { - response: JSON.stringify({ - assessment: "", - blockers: [], - nits: ["Add coverage for the new branch."], - suggestions: ["Add coverage for the new branch."], - }), - }; - }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await putCachedAiReview(env, "JSONbored/gittensory", 10, "a10", "block", { - notes: "**Nits (1)**\n- stale cached nit", - reviewerCount: 1, - }); - const commentBodies: string[] = []; - const checkPatches: Array<{ status?: string; conclusion?: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/10/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/10")) return Response.json({ number: 10, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a10" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a10/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a10/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/10/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/10/comments") && method === "POST") { - commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "")); - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 971 }, { status: 201 }); - if (url.includes("/check-runs/971") && method === "PATCH") { - checkPatches.push(JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }); - return Response.json({ id: 971 }); - } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "reviewing-placeholder-ai-summary-missing", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 10, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a10" }, labels: [], body: "Closes #1" }, - }, - }), - ).resolves.toBeUndefined(); - - expect(commentBodies.length).toBeGreaterThanOrEqual(2); - expect(commentBodies[0]).toContain("is reviewing"); - expect(commentBodies[0]).toContain("🟪"); - const finalComment = commentBodies.find((body) => !body.includes("is reviewing")); - expect(finalComment).toBeDefined(); - expect(finalComment).toContain("Readiness score"); - expect(finalComment).not.toContain("stale cached nit"); - expect(finalComment).toContain("did not include a separate narrative summary"); - expect(finalComment).toContain("Add coverage for the new branch."); - expect(aiCalls).toBeGreaterThan(0); - expect(checkPatches).toContainEqual(expect.objectContaining({ status: "completed" })); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?") - .bind("github_app.ai_review_public_summary_missing") - .first<{ n: number }>(); - expect(audit?.n).toBe(0); - }); - - it("publishes a non-cacheable AI-unavailable note when no reviewer returns usable output", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => ({ response: "not-json" }), - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 48, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a48" }, labels: [], body: "Closes #1" }); - const commentBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/48/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/48")) return Response.json({ number: 48, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a48" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a48/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a48/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/48/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/48/comments") && method === "POST") { - commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "")); - return Response.json({ id: 48 }, { status: 201 }); - } - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await expect( - processJob(env, { - type: "agent-regate-pr", - deliveryId: "regate-ai-unavailable", - repoFullName: "JSONbored/gittensory", - prNumber: 48, - installationId: 123, - }), - ).resolves.toBeUndefined(); - - expect(commentBodies.length).toBeGreaterThanOrEqual(2); - expect(commentBodies[0]).toContain("is reviewing"); - const finalComment = commentBodies.find((body) => !body.includes("is reviewing")); - expect(finalComment).toContain("Gittensory review needs maintainer review"); - expect(finalComment).toContain("AI review could not be completed for this PR head"); - expect(finalComment).not.toContain("The AI reviewer returned public review text but not the expected structured verdict"); - // #regate-churn: the "AI review could not be completed" outcome is now PERSISTED (so a repeated scheduled - // sweep pass at the same head can reuse it for a bounded cooldown instead of re-spending an LLM call every - // tick) but marked non-durable (cacheable=0) — it must never be replayed as a trustworthy, indefinitely-valid - // verdict. - const cached = await env.DB.prepare("select cacheable from ai_review_cache where repo_full_name = ? and pull_number = ?") - .bind("JSONbored/gittensory", 48) - .first<{ cacheable: number }>(); - expect(cached?.cacheable).toBe(0); - const nonCacheableAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?") - .bind("github_app.ai_review_non_cacheable") - .first<{ n: number }>(); - expect(nonCacheableAudit?.n).toBe(1); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?") - .bind("github_app.ai_review_public_summary_missing") - .first<{ n: number }>(); - expect(audit?.n).toBe(0); - }); - - it("INVARIANT (#confirmed-bug): a second overlapping pass for the same PR head defers to the AI review lock, holds the gate NEUTRAL, and never calls the AI a second time", async () => { - // Simulates the confirmed TOCTOU race: a webhook pass and an agent-regate-pr sweep pass both reach - // runAiReviewForAdvisory for the SAME PR at the SAME head SHA before either has written the cache. The - // webhook pass (not modeled directly here — job-coalesce keys never match across trigger shapes) is - // simulated by pre-claiming the lock exactly as runAiReviewForAdvisory itself would; the agent-regate-pr - // pass under test must then defer instead of firing its own, potentially-divergent LLM call. - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async () => { - aiCalls += 1; - return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; - }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 49, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a49" }, labels: [], body: "Closes #1" }); - const commentBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/49/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/49")) return Response.json({ number: 49, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a49" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a49/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a49/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/49/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/49/comments") && method === "POST") { - commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "")); - return Response.json({ id: 49 }, { status: 201 }); - } - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - // The "first pass" (webhook-shaped) claims the lock for this exact (repo, PR, head, mode) tuple and is still - // in-flight when the "second pass" (agent-regate-pr sweep-shaped) below reaches runAiReviewForAdvisory. - expect((await claimAiReviewLock(env, "JSONbored/gittensory", 49, "a49", "block")).acquired).toBe(true); - - await expect( - processJob(env, { - type: "agent-regate-pr", - deliveryId: "race-ai-review", - repoFullName: "JSONbored/gittensory", - prNumber: 49, - installationId: 123, - }), - ).resolves.toBeUndefined(); - - // The losing pass never called the AI a second time — it deferred to the lock instead of double-spending. - expect(aiCalls).toBe(0); - const finalComment = commentBodies.find((body) => !body.includes("is reviewing")); - expect(finalComment).toContain("Gittensory review needs maintainer review"); - expect(finalComment).toContain("AI review is already running for this PR head in another Gittensory pass"); - // A lock-contention placeholder must never be persisted at all (not even non-durably, #regate-churn) — the - // concurrent pass it deferred to writes the REAL result within seconds, and replaying this placeholder for - // the rest of a bounded-cooldown window would mask that real result long after the race resolved. - const cached = await env.DB.prepare("select count(*) as n from ai_review_cache where repo_full_name = ? and pull_number = ?") - .bind("JSONbored/gittensory", 49) - .first<{ n: number }>(); - expect(cached?.n).toBe(0); - }); - - it("publishes deterministic surface and reports missing summary when required AI is over quota", async () => { - const aiRun = vi.fn(async () => ({ response: "{}" })); - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: aiRun } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "0", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 49, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a49" }, labels: [], body: "Closes #1" }); - const commentBodies: string[] = []; - const captureSpy = vi.spyOn(sentryModule, "captureReviewFailure"); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/49/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/49")) return Response.json({ number: 49, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a49" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a49/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a49/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/49/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/49/comments") && method === "POST") { - commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "")); - return Response.json({ id: 49 }, { status: 201 }); - } - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await expect( - processJob(env, { - type: "agent-regate-pr", - deliveryId: "regate-ai-over-quota", - repoFullName: "JSONbored/gittensory", - prNumber: 49, - installationId: 123, - }), - ).resolves.toBeUndefined(); - - expect(aiRun).not.toHaveBeenCalled(); - expect(commentBodies.length).toBeGreaterThanOrEqual(2); - const finalComment = commentBodies.find((body) => !body.includes("is reviewing")); - expect(finalComment).toContain("Readiness score"); - expect(finalComment).not.toContain("AI review returned public review text"); - const audit = await env.DB.prepare("select event_type, metadata_json from audit_events where event_type = ?") - .bind("github_app.ai_review_public_summary_missing") - .first<{ event_type: string; metadata_json: string }>(); - expect(audit).toMatchObject({ event_type: "github_app.ai_review_public_summary_missing" }); - expect(audit?.metadata_json).toContain('"aiReviewMode":"block"'); - expect(captureSpy).toHaveBeenCalledWith( - expect.any(Error), - expect.objectContaining({ - reason: "ai_review_public_summary_missing", - repo: "JSONbored/gittensory", - pr: 49, - reviewer_count: 0, - public_notes: false, - }), - ); - captureSpy.mockRestore(); - }); - - it("agent re-gate sweep re-reviews each stale open PR (installation id) and swallows a failing re-review", async () => { - const env = createTestEnv({}); - await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, linkedIssueGateMode: "block" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Unlinked PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "no linked issue here" }); - // Advance past the one-hour freshness window so the just-seeded PR reads as stale. - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - // Make the re-review itself REJECT (its advisory persist throws) so the sweep's per-PR error backstop runs. - // Only the advisories insert is poisoned; every other read/write (verdict computation, the closing audit - // event) keeps working — the sweep must still complete and record its advisory verdict. - const realPrepare = env.DB.prepare.bind(env.DB); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - env.DB.prepare = ((sql: string) => { - if (/insert\s+into\s+["'`]?advisories/i.test(sql)) throw new Error("advisory persist failed"); - return realPrepare(sql); - }) as typeof env.DB.prepare; - - await sweepAndDrainPerPr(env, "owner/agent-repo"); - - const audit = await realPrepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ - outcome: string; - metadata_json: string; - }>(); - expect(audit?.outcome).toBe("completed"); - expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ repoFullName: "owner/agent-repo", examined: 1, flagged: 1 }); - // The failing re-review was caught and logged via the sweep_rereview_failed backstop, not rethrown. - expect(errors.mock.calls.some((call) => String(call[0]).includes("sweep_rereview_failed"))).toBe(true); - errors.mockRestore(); - }); - - it("agent re-gate sweep stamps last_regated_at on each recomputed PR so the next sweep advances (#audit-sweep-converge)", async () => { - const env = createTestEnv({}); - await upsertInstallation(env, { action: "created", installation: { id: 9002, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9002); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); - const before = await env.DB.prepare("select last_regated_at from pull_requests where repo_full_name = ? and number = 7").bind("owner/agent-repo").first<{ last_regated_at: string | null }>(); - expect(before?.last_regated_at).toBeNull(); // never swept yet - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - // #2852: autonomy configured (merge: auto) now means the gate CONCLUSION is evaluated even without a - // GITHUB_APP_PRIVATE_KEY / check-run publish, which reaches the review-thread-blockers live fetch -- stub a - // generic safe response so that call resolves instead of hitting a real, unmocked network request. - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url === "https://api.github.com/graphql") return Response.json({ data: {} }); - return Response.json({}); - }); - - await sweepAndDrainPerPr(env, "owner/agent-repo"); - - const after = await env.DB.prepare("select last_regated_at from pull_requests where repo_full_name = ? and number = 7").bind("owner/agent-repo").first<{ last_regated_at: string | null }>(); - expect(typeof after?.last_regated_at).toBe("string"); // stamped via a D1 write at dispatch — convergence does not need a GitHub write - }); - - it("agent re-gate sweep processes strict staleness order even when a PR is missing its current Gate check (#selfhost-fifo-ordering)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9400, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9400); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - for (const number of [1, 2, 3, 4]) { - const headSha = `a${number}`; - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `PR${number}`, state: "open", user: { login: "c" }, head: { sha: headSha }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", number, headSha); - if (number !== 2) { - await upsertCheckSummary(env, { - id: `gate-${number}`, - repoFullName: "owner/agent-repo", - pullNumber: number, - headSha, - name: "Gittensory Orb Review Agent", - status: "completed", - conclusion: "success", - payload: {}, - }); - } - } - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 5, title: "Draft without a head", state: "open", draft: true, user: { login: "c" }, labels: [], body: "" } as never); - // Only PR2 gets a regate stamp (post-#never-endless-reregate, an ordinary already-regated PR is permanently - // excluded from the sweep -- see agent-sweep.test.ts -- so PR1/3/4 must stay never-regated to remain eligible - // ordinary candidates at all). PR2 is missing its current Gate check (surfaceRepairPriorityPullNumbers would - // flag it as a repair candidate), so its repair-priority bypass keeps it eligible DESPITE already having a - // stamp -- this is exactly the scenario the repair-priority bypass exists for. - await env.DB.prepare( - `update pull_requests set last_regated_at = '2026-05-28T01:50:00.000Z' where repo_full_name = ? and number = 2`, - ) - .bind("owner/agent-repo") - .run(); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const fanned = sent.filter((job) => job.type === "agent-regate-pr"); - // PR2 is missing its current Gate check and already has a regate stamp from 10 min ago (very fresh by - // lastRegatedAt), while PR1/3/4 have never been regated at all (the ordinary, post-#never-endless-reregate - // candidate shape). An earlier revision sorted repair candidates first regardless of staleness, jumping PR2 - // to the front of this batch -- that let a PR needing repair cut ahead of older PRs that merely went stale, - // observed live as PRs dispatching out of order ("spraying") whenever a repo had a mixed repair/ordinary - // backlog. Repair status only affects ELIGIBILITY (staying in the pool despite already having a stamp), - // never final order, so PR2 takes its rightful (last, since it's the freshest-regated) place and is dropped - // by the max:3 cap this round -- same as it would be with no repair flag at all. - expect(fanned.map((job) => (job as Extract).prNumber)).toEqual([1, 3, 4]); - }); - - it("REGRESSION (#3815): regateSweepOrderMode 'oldest-first' fans out per-PR jobs in creation order with a monotonic delaySeconds stagger", async () => { - const dispatched: { prNumber: number; delaySeconds: number | undefined }[] = []; - const env = createTestEnv({ - JOBS: { - async send(m: import("../../src/types").JobMessage, options?: { delaySeconds?: number }) { - if (m.type === "agent-regate-pr") dispatched.push({ prNumber: m.prNumber, delaySeconds: options?.delaySeconds ?? 0 }); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9403, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9403); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, regateSweepOrderMode: "oldest-first", gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - // Deliberately seeded out of PR-number order: #1 is the NEWEST, #3 is the OLDEST — proves the fan-out - // follows createdAt, not insertion/number order. - const created: Record = { 1: "2026-05-20T00:00:00.000Z", 2: "2026-05-10T00:00:00.000Z", 3: "2026-05-01T00:00:00.000Z" }; - for (const number of [1, 2, 3]) { - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { - number, - title: `PR${number}`, - state: "open", - user: { login: "c" }, - head: { sha: `a${number}` }, - labels: [], - body: "", - created_at: created[number]!, - updated_at: created[number]!, - }); - } - vi.setSystemTime(new Date("2026-05-28T00:00:00.000Z")); // well past the 2-min webhook-freshness window for all three - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - expect(dispatched.map((d) => d.prNumber)).toEqual([3, 2, 1]); // oldest-created (#3) first, newest (#1) last - expect(dispatched.map((d) => d.delaySeconds)).toEqual([0, 10, 20]); // strictly increasing with dispatch order - }); - - it("REGRESSION: scheduled sweeps repair every missing current Gate check without waiting behind another repo backlog", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - snapshot() { - return { - totals: { pending: 0, processing: 1, dead: 0, due: 0 }, - byType: [ - { - type: "agent-regate-pr", - status: "processing", - count: 1, - due: 0, - }, - ], - }; - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9402, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9402); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - for (const number of [1, 2, 3, 4, 5]) { - const headSha = `repair-${number}`; - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `Repair ${number}`, state: "open", user: { login: "c" }, head: { sha: headSha }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", number, headSha); - } - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); - expect(fanned.map((job) => job.prNumber)).toEqual([1, 2, 3, 4, 5]); - const audit = await env.DB.prepare("select metadata_json from audit_events where event_type = ? and outcome = ?") - .bind("agent.sweep.regate", "completed") - .first<{ metadata_json: string }>(); - expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ - repoFullName: "owner/agent-repo", - examined: 5, - }); - }); - - it("REGRESSION: an active per-PR regate backlog restricts the sweep to priority repairs, not a full stale-PR batch too", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - // A nonzero per-PR regate backlog (agent-regate-pr pending/processing > 0) -- the same signal the - // "waiting behind another repo backlog" deferral above reacts to. - snapshot() { - return { - totals: { pending: 1, processing: 0, dead: 0, due: 1 }, - byType: [{ type: "agent-regate-pr", status: "pending", count: 1, due: 1 }], - }; - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9403, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9403); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - // PR 1: missing its current Gate check -- the one priority repair. Make it newer-by-regate than the - // ordinary stale PRs below, reproducing the backlog bug where a max=1 staleness slice could drop the repair. - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 1, title: "Repair 1", state: "open", user: { login: "c" }, head: { sha: "repair-1" }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 1, "repair-1"); - await env.DB.prepare("update pull_requests set last_regated_at = ? where repo_full_name = ? and number = ?") - .bind("2026-05-28T01:59:00.000Z", "owner/agent-repo", 1) - .run(); - // PRs 2-5: ordinary, already-current, stale-by-time PRs -- a normal (no-backlog) sweep would pick these up - // too, but while the backlog is draining they must sit out so the sweep only carries the priority repair. - for (const number of [2, 3, 4, 5]) { - const headSha = `stale-${number}`; - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `Stale ${number}`, state: "open", user: { login: "c" }, head: { sha: headSha }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", number, headSha); - await env.DB.prepare("update pull_requests set last_regated_at = ? where repo_full_name = ? and number = ?") - .bind(`2026-05-28T01:0${number}:00.000Z`, "owner/agent-repo", number) - .run(); - await upsertCheckSummary(env, { - id: `gate-current-${number}`, - repoFullName: "owner/agent-repo", - pullNumber: number, - headSha, - name: "Gittensory Orb Review Agent", - status: "completed", - conclusion: "success", - payload: {}, - }); - } - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); - expect(fanned.map((job) => job.prNumber)).toEqual([1]); // only the priority repair, not PRs 2-5 - }); - - it("REGRESSION: the sweep tags a priority-repair fan-out with 'regate-repair:' and an ordinary candidate with 'regate-sweep:' (#selfhost-queue-liveness)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9404, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9404); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - // PR 1: missing its current Gate check for its current head -- surfaceRepairPriorityPullNumbers flags this as - // outage-repair priority (no completed Gittensory Gate check run at the live head SHA). - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 1, title: "Repair 1", state: "open", user: { login: "c" }, head: { sha: "repair-1" }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 1, "repair-1"); - // PR 2: ordinary PR with a completed current-head Gate check -- NOT priority. - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 2, title: "Ordinary 2", state: "open", user: { login: "c" }, head: { sha: "ordinary-2" }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 2, "ordinary-2"); - await upsertCheckSummary(env, { - id: "gate-current-2", - repoFullName: "owner/agent-repo", - pullNumber: 2, - headSha: "ordinary-2", - name: "Gittensory Orb Review Agent", - status: "completed", - conclusion: "success", - payload: {}, - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); - expect(fanned).toHaveLength(2); - const repairJob = fanned.find((job) => job.prNumber === 1); - const ordinaryJob = fanned.find((job) => job.prNumber === 2); - expect(repairJob).toMatchObject({ - type: "agent-regate-pr", - deliveryId: "regate-repair:owner/agent-repo#1", - repoFullName: "owner/agent-repo", - prNumber: 1, - installationId: 9404, - }); - expect(ordinaryJob).toMatchObject({ - type: "agent-regate-pr", - deliveryId: "regate-sweep:owner/agent-repo#2", - repoFullName: "owner/agent-repo", - prNumber: 2, - installationId: 9404, - }); - }); - - it("REGRESSION (#orb-retry-storm): after MAX_ATTEMPTS repair dispatches for the SAME head SHA, the sweep stops bypassing freshness and records exactly one repair_exhausted audit event", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9407, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9407); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - // PR 1: missing its current Gate check for its current head -- would ordinarily be flagged outage-repair - // priority on every tick. Pre-seed REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA=5 (#3998) prior repair-attempt audit - // events for this EXACT head SHA to simulate a review that keeps failing (e.g. a timeout) and never - // publishes a completed gate check. - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 1, title: "Stuck repair", state: "open", user: { login: "c" }, head: { sha: "stuck-sha" }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 1, "stuck-sha"); - const targetKey = "owner/agent-repo#1#stuck-sha"; - for (let i = 0; i < 5; i += 1) { - await repositoriesModule.recordAuditEvent(env, { - eventType: "agent.sweep.regate.repair_attempt", - actor: "gittensory", - targetKey, - outcome: "queued", - detail: "prior attempt", - metadata: {}, - }); - } - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - - try { - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - // No longer treated as priority repair -- either not fanned at all, or fanned as an ordinary "regate-sweep:" - // candidate, but never re-dispatched as "regate-repair:" once the same SHA has exhausted its attempt budget. - const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); - expect(fanned.every((job) => job.deliveryId !== "regate-repair:owner/agent-repo#1")).toBe(true); - const exhausted = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") - .bind("agent.sweep.regate.repair_exhausted", targetKey) - .first<{ n: number }>(); - expect(exhausted?.n).toBe(1); - // No further repair-attempt event was recorded for the exhausted SHA this tick. - const attempts = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") - .bind("agent.sweep.regate.repair_attempt", targetKey) - .first<{ n: number }>(); - expect(attempts?.n).toBe(5); - // Sentry-visible signal (via the structured-log forwarder) fires exactly once alongside the audit event. - const exhaustedLogs = errors.mock.calls.filter(([line]) => typeof line === "string" && line.includes("regate_repair_exhausted")); - expect(exhaustedLogs).toHaveLength(1); - const logged = JSON.parse(exhaustedLogs[0]![0] as string) as Record; - expect(logged).toMatchObject({ level: "error", event: "regate_repair_exhausted", repo: "owner/agent-repo", pullNumber: 1, headSha: "stuck-sha", attempts: 5 }); - } finally { - errors.mockRestore(); - } - }, 60_000); - - it("REGRESSION (#orb-retry-storm): a repair dispatch under the attempt cap records a repair_attempt audit event, and a second sweep tick does not duplicate the repair_exhausted event once already flagged", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9408, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9408); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 2, title: "Fresh repair", state: "open", user: { login: "c" }, head: { sha: "fresh-sha" }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 2, "fresh-sha"); - const targetKey = "owner/agent-repo#2#fresh-sha"; - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); - expect(fanned.map((job) => job.deliveryId)).toContain("regate-repair:owner/agent-repo#2"); - // #orb-retry-storm (#3998): repair_attempt is now recorded at EXECUTION time (inside regatePullRequest, - // after rate-limit admission), not at dispatch time -- a deferred/dropped fan-out no longer counts against - // the cap. The sweep only dispatches the per-PR job above; it must actually run for the attempt to land. - await processJob(env, fanned.find((job) => job.deliveryId === "regate-repair:owner/agent-repo#2")!); - const attemptsAfterFirst = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") - .bind("agent.sweep.regate.repair_attempt", targetKey) - .first<{ n: number }>(); - expect(attemptsAfterFirst?.n).toBe(1); - - // Manually push this SHA over the REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA=5 cap (#3998), then run the sweep twice - // more -- the exhausted event must be recorded only once even though the PR is (re-)evaluated on every tick. - for (let i = 0; i < 4; i += 1) { - await repositoriesModule.recordAuditEvent(env, { - eventType: "agent.sweep.regate.repair_attempt", - actor: "gittensory", - targetKey, - outcome: "queued", - detail: "prior attempt", - metadata: {}, - }); - } - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const exhausted = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") - .bind("agent.sweep.regate.repair_exhausted", targetKey) - .first<{ n: number }>(); - expect(exhausted?.n).toBe(1); - }, 60_000); - - it("agent re-gate sweep fail-opens when current Gate check reads fail during repair priority selection", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9401, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9401); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Repair me", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - const realPrepare = env.DB.prepare.bind(env.DB); - env.DB.prepare = ((sql: string) => { - if (/from\s+["`]?check_summaries["`]?/i.test(sql)) throw new Error("check summary read failed"); - return realPrepare(sql); - }) as typeof env.DB.prepare; - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const fanned = sent.filter((job) => job.type === "agent-regate-pr") as Extract[]; - expect(fanned.map((job) => job.prNumber)).toEqual([7]); - }); - - it("scheduled sweeps skip open-PR refresh when an allowlisted repo has not been registered locally yet", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - GITTENSORY_REVIEW_REPOS: "owner/missing-repo", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - const segmentSpy = vi.spyOn(repositoriesModule, "getRepoSyncSegment"); - const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment"); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/missing-repo" }); - - expect(segmentSpy).not.toHaveBeenCalled(); - expect(backfillSpy).not.toHaveBeenCalled(); - expect(sent).toEqual([]); - segmentSpy.mockRestore(); - backfillSpy.mockRestore(); - }); - - it("scheduled sweeps can refresh stale open-PR rows with an Orb enrollment credential", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - ORB_ENROLLMENT_SECRET: "orb-secret", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9406, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9406); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertRepoSyncSegment(env, completeSegment("owner/agent-repo", "open_pull_requests")); - const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockResolvedValueOnce({ - ok: true, - repoFullName: "owner/agent-repo", - segment: "open_pull_requests", - status: "complete", - fetchedCount: 0, - warnings: [], - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests", force: true })); - expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); - backfillSpy.mockRestore(); - }); - - it("REGRESSION: scheduled sweeps refresh stale open-PR rows so missed webhooks cannot hide PRs from repair", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9402, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9402); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertRepoSyncSegment(env, completeSegment("owner/agent-repo", "open_pull_requests")); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.github.com/graphql") { - return Response.json({ - data: { - rateLimit: { remaining: 4999, resetAt: "2026-05-28T03:00:00.000Z" }, - repository: { - issues: { totalCount: 0 }, - openPullRequests: { totalCount: 1 }, - mergedPullRequests: { totalCount: 0 }, - closedPullRequests: { totalCount: 0 }, - labels: { totalCount: 0 }, - }, - }, - }); - } - if (url.includes("/pulls?state=open")) { - return Response.json([ - { - number: 11, - title: "Webhook missed this PR", - state: "open", - user: { login: "contributor" }, - head: { sha: "h11" }, - labels: [], - body: "Fixes #1", - created_at: "2026-05-27T00:00:00.000Z", - updated_at: "2026-05-27T00:00:00.000Z", - }, - ]); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect((await getPullRequest(env, "owner/agent-repo", 11))?.headSha).toBe("h11"); - const fanned = sent.filter((job) => job.type === "agent-regate-pr") as Extract[]; - expect(fanned.map((job) => job.prNumber)).toEqual([11]); - expect(sent.some((job) => job.type === "backfill-pr-details" && job.repoFullName === "owner/agent-repo")).toBe(true); - }); - - it("scheduled sweeps do not duplicate an active open-PR refresh", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9403, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9403); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertRepoSyncSegment(env, { - ...completeSegment("owner/agent-repo", "open_pull_requests"), - status: "running", - }); - const fetchSpy = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Response.json([])); - vi.stubGlobal("fetch", fetchSpy); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect( - fetchSpy.mock.calls - .map((call) => String((call as [RequestInfo | URL, RequestInit?])[0])) - .filter((url) => url === "https://api.github.com/graphql" || url.includes("/pulls?state=open")), - ).toEqual([]); - expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); - }); - - it("scheduled sweeps fail open when open-PR sync state reads and refreshes fail", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9404, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9404); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - const segmentSpy = vi.spyOn(repositoriesModule, "getRepoSyncSegment").mockRejectedValueOnce(new Error("segment read failed")); - const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockRejectedValueOnce(new Error("open PR refresh failed")); - const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests", mode: "light", force: true })); - expect(warn.mock.calls.some((call) => String(call[0]).includes("sweep_open_pr_sync_failed"))).toBe(true); - expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); - segmentSpy.mockRestore(); - backfillSpy.mockRestore(); - warn.mockRestore(); - }); - - it("REGRESSION (#sweep-uninstalled-budget-waste): a scheduled sweep never refreshes open PRs (via the shared GITHUB_PUBLIC_TOKEN) for a registered-but-uninstalled repo, since no per-PR fan-out will ever follow", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - // Registered (e.g. via the subnet registry sync) but NOT installed — no installationId. - await upsertRepositoryFromGitHub(env, { name: "no-install", full_name: "owner/no-install", private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/no-install", autonomy: { merge: "auto" } }); - const segmentSpy = vi.spyOn(repositoriesModule, "getRepoSyncSegment"); - const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment"); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/no-install" }); - - expect(segmentSpy).not.toHaveBeenCalled(); - expect(backfillSpy).not.toHaveBeenCalled(); - expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); - segmentSpy.mockRestore(); - backfillSpy.mockRestore(); - }); - - it("scheduled sweeps DO still refresh open PRs for an installed repo even when GITHUB_PUBLIC_TOKEN is also configured (installation presence gates the skip, not credential kind)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9405, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9405); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockResolvedValueOnce(undefined as never); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests", mode: "light", force: true })); - backfillSpy.mockRestore(); - }); - - it("scheduled sweeps refresh incomplete open-PR sync segments", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9405, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9405); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertRepoSyncSegment(env, { - ...completeSegment("owner/agent-repo", "open_pull_requests"), - status: "partial", - }); - const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockResolvedValueOnce({ - ok: true, - repoFullName: "owner/agent-repo", - segment: "open_pull_requests", - status: "complete", - fetchedCount: 0, - warnings: [], - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests" })); - expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); - backfillSpy.mockRestore(); - }); - - it("scheduled sweeps refresh completed open-PR sync rows whose completion time is missing", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9407, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9407); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - const segmentSpy = vi.spyOn(repositoriesModule, "getRepoSyncSegment").mockResolvedValueOnce({ - ...completeSegment("owner/agent-repo", "open_pull_requests"), - completedAt: undefined, - } as never); - const backfillSpy = vi.spyOn(backfillModule, "backfillRepositorySegment").mockResolvedValueOnce({ - ok: true, - repoFullName: "owner/agent-repo", - segment: "open_pull_requests", - status: "complete", - fetchedCount: 0, - warnings: [], - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect(backfillSpy).toHaveBeenCalledWith(env, expect.objectContaining({ segment: "open_pull_requests", force: true })); - expect(sent.filter((job) => job.type === "agent-regate-pr")).toEqual([]); - segmentSpy.mockRestore(); - backfillSpy.mockRestore(); - }); - - it("REGRESSION (#audit-sweep-dispatch-stamp): ONE sweep stamps ALL candidates AT DISPATCH, so the next fan-out skips the repo as draining — no overlapping sweeps", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9300, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9300); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - for (const number of [7, 8, 9]) { - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `PR${number}`, state: "open", user: { login: "c" }, head: { sha: `a${number}` }, labels: [], body: "" }); - } - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - // Run ONE sweep — but do NOT drain the per-PR jobs (simulate the staggered/deferred re-reviews not having run yet). - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - // The marker is stamped for EVERY candidate immediately at dispatch — NOT waiting on the per-PR jobs. - const stamped = await env.DB.prepare("select count(*) as n from pull_requests where repo_full_name = ? and last_regated_at is not null").bind("owner/agent-repo").first<{ n: number }>(); - expect(stamped?.n).toBe(3); - - // So the very next cron fan-out sees the fresh stamp and SKIPS this repo as draining — the overlap that caused the runaway is gone. - sent.length = 0; - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule" }); - expect(sent.some((m) => m.type === "agent-regate-sweep" && m.repoFullName === "owner/agent-repo")).toBe(false); - const fanout = await env.DB.prepare("select metadata_json from audit_events where event_type = ? order by created_at desc limit 1").bind("agent.sweep.fanout").first<{ metadata_json: string }>(); - expect(JSON.parse(fanout?.metadata_json ?? "{}").skippedDraining).toBeGreaterThanOrEqual(1); - }); - - it("agent re-gate sweep swallows a failing last_regated_at stamp and still completes (#audit-sweep-converge)", async () => { - const env = createTestEnv({}); - await upsertInstallation(env, { action: "created", installation: { id: 9003, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9003); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - // #2852: autonomy configured (merge: auto) now means the gate CONCLUSION is evaluated even without a - // GITHUB_APP_PRIVATE_KEY / check-run publish, which reaches the review-thread-blockers live fetch -- stub a - // generic safe response so that call resolves instead of hitting a real, unmocked network request. - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url === "https://api.github.com/graphql") return Response.json({ data: {} }); - return Response.json({}); - }); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - const stamp = vi.spyOn(repositoriesModule, "markPullRequestsRegated").mockRejectedValueOnce(new Error("D1 write error")); - - await sweepAndDrainPerPr(env, "owner/agent-repo"); - - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); // the sweep still records its verdict; the dispatch-time stamp failure is swallowed - expect(errors.mock.calls.some((call) => String(call[0]).includes("sweep_mark_regated_failed"))).toBe(true); - stamp.mockRestore(); - errors.mockRestore(); - }); - - it("agent re-gate sweep respects the #776 kill-switch: a paused repo records a skip and recomputes nothing (#777)", async () => { - const env = createTestEnv({}); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, agentPaused: true }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale PR", state: "open", user: { login: "contributor" }, head: { sha: "abc" }, labels: [], body: "x" }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const audit = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ - outcome: string; - detail: string; - metadata_json: string; - }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toMatch(/paused/i); - expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ mode: "paused" }); - }); - - it("agent re-gate sweep no-ops safely on a missing repo arg or an un-configured repo (#777)", async () => { - const env = createTestEnv({}); - // (a) a test-mode per-repo job with no repoFullName → defensive early return - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test" }); - // (b) a repo that never opted the agent in → defensive return after settings resolve - await upsertRepositoryFromGitHub(env, { name: "plain-repo", full_name: "owner/plain-repo", private: false, owner: { login: "owner" } }); - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/plain-repo" }); - - const count = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ n: number }>(); - expect(count?.n).toBe(0); - }); - - it("agent re-gate sweep stays quiet when no open PR is stale enough to re-gate (#777)", async () => { - const env = createTestEnv({}); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - // Seeded "now" → within the freshness window → not a candidate; no clock advance. - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Fresh PR", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "x" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); - await upsertCheckSummary(env, { - id: "gate-fresh-7", - repoFullName: "owner/agent-repo", - pullNumber: 7, - headSha: "a7", - name: "Gittensory Orb Review Agent", - status: "completed", - conclusion: "success", - payload: {}, - }); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const count = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ n: number }>(); - expect(count?.n).toBe(0); - }); - - it("INVARIANT: the sweep fans out one agent-regate-pr job per candidate onto the JOBS lane, not inline (#audit-sweep-fanout)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9100, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9100); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 8, title: "PR8", state: "open", user: { login: "c" }, head: { sha: "a8" }, labels: [], body: "" }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const perPr = sent.filter((m): m is Extract => m.type === "agent-regate-pr"); - expect(perPr.map((m) => m.prNumber).sort()).toEqual([7, 8]); // one per candidate - expect(perPr.every((m) => m.installationId === 9100 && m.repoFullName === "owner/agent-repo")).toBe(true); - expect(sent.every((m) => m.type === "agent-regate-pr")).toBe(true); // the heavy work is enqueued, never done inline - }); - - it("INVARIANT (in-flight guard): the fan-out SKIPS a repo whose prior sweep is still draining, enqueues an idle one (#audit-sweep-fanout)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ GITTENSORY_REVIEW_REPOS: "", JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9101, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - for (const name of ["draining", "idle"]) { - await upsertRepositoryFromGitHub(env, { name, full_name: `owner/${name}`, private: false, owner: { login: "owner" } }, 9101); - await upsertRepositorySettings(env, { repoFullName: `owner/${name}`, autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, `owner/${name}`, { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "h1" }, labels: [], body: "" }); - } - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - // owner/draining was just regated (a sweep is mid-drain); owner/idle has never been swept. - await repositoriesModule.markPullRequestRegated(env, "owner/draining", 1); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule" }); // no repoFullName → fan-out path - - const sweepRepos = sent.filter((m): m is Extract => m.type === "agent-regate-sweep").map((m) => m.repoFullName); - expect(sweepRepos).toEqual(["owner/idle"]); // the draining repo is skipped, the idle one enqueued - const fanout = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("agent.sweep.fanout").first<{ metadata_json: string }>(); - expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 1, skippedDraining: 1 }); - }); - - it("INVARIANT (#audit-fanout-dedup): a BURST of fan-outs collapses to ONE — the second claims nothing and audits denied", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9400, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9400); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule" }); // first fan-out claims the window - expect(sent.some((m) => m.type === "agent-regate-sweep" && m.repoFullName === "owner/agent-repo")).toBe(true); - - sent.length = 0; - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule" }); // burst sibling in the same window → deduped - expect(sent.filter((m) => m.type === "agent-regate-sweep")).toEqual([]); // enqueues no redundant sweep - const denied = await env.DB.prepare("select count(*) as n from audit_events where event_type='agent.sweep.fanout' and outcome='denied'").first<{ n: number }>(); - expect(denied?.n).toBe(1); - }); - - it("claimAiReviewLock claims when free, denies when held (per-PR+head+mode, not globally), and release frees it again (#confirmed-bug)", async () => { - const env = createTestEnv({}); - // First claim for this exact (repo, PR, head, mode) succeeds — no prior pass in-flight. - const first = await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"); - expect(first.acquired).toBe(true); - // A second, concurrent pass for the SAME PR at the SAME head and mode (regardless of what triggered it — - // webhook or sweep) is denied while the first is still in-flight — exactly the race this lock exists for. - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(false); - // A DIFFERENT head SHA for the same PR is unaffected — a new commit is a genuinely new review, not a dup. - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha2", "block")).acquired).toBe(true); - // A DIFFERENT mode for the same PR+head is also unaffected — advisory vs block are independent lock keys. - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "advisory")).acquired).toBe(true); - // A DIFFERENT PR in the same repo is unaffected — the lock is per-PR+head+mode, not repo-wide. - expect((await claimAiReviewLock(env, "owner/agent-repo", 8, "sha1", "block")).acquired).toBe(true); - // Release (the finally block's job) frees the (PR, head, mode) tuple — a subsequent pass can claim it again. - await releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", first.ownerToken); - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); - }); - - it("claimAiReviewLock fails OPEN on a broken transient cache — never itself blocks a real review from running (#confirmed-bug)", async () => { - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async () => { throw new Error("cache read error"); }, - set: async () => { throw new Error("cache write error"); }, - del: async () => { throw new Error("cache delete error"); }, - }, - }); - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); - await expect(releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", null)).resolves.toBeUndefined(); - }); - - it("claimAiReviewLock fails OPEN when no transient cache is configured at all — nothing to serialize against (#confirmed-bug)", async () => { - const env = createTestEnv({}); - delete env.SELFHOST_TRANSIENT_CACHE; - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); - }); - - it("claimAiReviewLock fails OPEN when the atomic claim primitive itself throws (#confirmed-bug)", async () => { - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async () => null, - set: async () => undefined, - claim: async () => { throw new Error("redis unavailable"); }, - }, - }); - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); - }); - - it("REGRESSION: claimAiReviewLock uses an atomic check-and-set, so two genuinely concurrent claims for the SAME (repo, PR, head, mode) can never both succeed", async () => { - // A get-then-set pair has a window between the read and the write where two concurrent callers can both - // observe an absent key and both claim it — exactly what this lock exists to prevent (a webhook pass and a - // sweep pass both missing the cache and both firing a real LLM call). This test races two claims for the - // same tuple via Promise.all (both kick off before either resolves) against the default test cache's - // claim(), which mirrors createRedisCache's atomic SET NX: the check-and-set happens with no `await` - // boundary in between, so it is impossible for both callers to see "unclaimed". - const env = createTestEnv({}); - const [first, second] = await Promise.all([ - claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"), - claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"), - ]); - expect([first, second].filter((claim) => claim.acquired)).toHaveLength(1); - }); - - it("REGRESSION: claimAiReviewLock calls the atomic claim primitive, not a separate get+set pair, when the cache supports it", async () => { - const calls: string[] = []; - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async () => { calls.push("get"); return null; }, - set: async () => { calls.push("set"); }, - claim: async () => { calls.push("claim"); return true; }, - releaseIfValue: async () => true, - }, - }); - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); - expect(calls).toEqual(["claim"]); // never falls through to the racy get/set pair when claim is available - }); - - it("claimAiReviewLock returns true unconditionally when the cache has no claim() — no false exclusivity guarantee (#confirmed-bug, review round 2)", async () => { - // A prior version of this helper fell back to a get-then-set pair (even with an extra write-then-verify - // re-read) when claim() wasn't available. That is NOT a real exclusivity guarantee: caller A can write its - // own token, read it straight back, and return true entirely before caller B's later write/read also - // completes and also returns true -- both callers "win". Rather than pretend to serialize via a check that - // silently fails under exactly the concurrent load this lock exists to guard against (duplicate LLM calls), - // a cache without claim() now gets NO exclusivity at all -- every call proceeds, sequential or concurrent, - // even for a key a previous call already "set" via get/set. - const values = new Map(); - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async (key: string) => values.get(key) ?? null, - set: async (key: string, value: string) => { values.set(key, value); }, - }, - }); - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); - expect((await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block")).acquired).toBe(true); - }); - - it("REGRESSION (#confirmed-bug, review round 2): claimAiReviewLock does not falsely claim exclusivity for two genuinely concurrent callers when the cache has no claim()", async () => { - // Documents the corrected, honest contract under the exact interleaving the gate flagged: with no atomic - // claim() primitive, BOTH concurrent callers proceed (true) -- a webhook pass and a sweep pass racing for - // the same PR head both fire their LLM call, same as before this lock existed, rather than one of them - // wrongly believing it has exclusive ownership when it doesn't. - const values = new Map(); - const yieldThenRun = (fn: () => T): Promise => new Promise((resolve) => queueMicrotask(() => resolve(fn()))); - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: (key: string) => yieldThenRun(() => values.get(key) ?? null), - set: (key: string, value: string) => yieldThenRun(() => { values.set(key, value); }), - }, - }); - const [first, second] = await Promise.all([ - claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"), - claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"), - ]); - expect([first.acquired, second.acquired]).toEqual([true, true]); - }); - - // claimPrActuationLock (#2129/#2135) is the ONE shared per-PR actuation lock: maybeRunAgentMaintenance, - // maybeCloseDraftDodgeAttempt, and maybeRecloseDisallowedReopen all claim/release the SAME key so none of the - // three mutating PR paths can race any other (review round 4) — a single namespace, not one lock per path. - it("claimPrActuationLock claims when free, denies when held (per-PR), and release frees it again (#2135)", async () => { - const env = createTestEnv({}); - const first = await claimPrActuationLock(env, "owner/act-repo", 7); - expect(first.acquired).toBe(true); - expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(false); - expect((await claimPrActuationLock(env, "owner/act-repo", 8)).acquired).toBe(true); - await releasePrActuationLock(env, "owner/act-repo", 7, first.ownerToken); - expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); - }); - - it("claimPrActuationLock fails OPEN on a broken transient cache — never itself blocks actuation (#2135)", async () => { - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async () => { throw new Error("cache read error"); }, - set: async () => { throw new Error("cache write error"); }, - del: async () => { throw new Error("cache delete error"); }, - }, - }); - expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); - await expect(releasePrActuationLock(env, "owner/act-repo", 7, null)).resolves.toBeUndefined(); - }); - - it("claimPrActuationLock fails OPEN when the atomic claim primitive itself throws (#2135)", async () => { - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async () => null, - set: async () => undefined, - claim: async () => { throw new Error("redis unavailable"); }, - }, - }); - expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); - }); - - it("REGRESSION (#2135): claimPrActuationLock uses an atomic check-and-set, so two genuinely concurrent claims for the SAME PR can never both succeed", async () => { - const env = createTestEnv({}); - const [first, second] = await Promise.all([ - claimPrActuationLock(env, "owner/act-repo", 7), - claimPrActuationLock(env, "owner/act-repo", 7), - ]); - expect([first, second].filter((claim) => claim.acquired)).toHaveLength(1); - }); - - it("REGRESSION (#2135): claimPrActuationLock calls the atomic claim primitive, not a separate get+set pair, when the cache supports it", async () => { - const calls: string[] = []; - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async () => { calls.push("get"); return null; }, - set: async () => { calls.push("set"); }, - claim: async () => { calls.push("claim"); return true; }, - releaseIfValue: async () => true, - }, - }); - expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); - expect(calls).toEqual(["claim"]); // never falls through to the racy get/set pair when claim is available - }); - - it("claimPrActuationLock returns true unconditionally when the cache has no claim() — no false exclusivity guarantee (#2135, review round 2)", async () => { - // A get-then-set pair (even with a re-read) is not a real exclusivity guarantee under concurrent load, so a - // cache without claim() now gets NO exclusivity at all rather than a fallback that only looks atomic. - const values = new Map(); - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async (key: string) => values.get(key) ?? null, - set: async (key: string, value: string) => { values.set(key, value); }, - }, - }); - expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); - expect((await claimPrActuationLock(env, "owner/act-repo", 7)).acquired).toBe(true); - }); - - it("REGRESSION (#2135, review round 2): claimPrActuationLock does not falsely claim exclusivity for two genuinely concurrent callers when the cache has no claim()", async () => { - const values = new Map(); - const yieldThenRun = (fn: () => T): Promise => new Promise((resolve) => queueMicrotask(() => resolve(fn()))); - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: (key: string) => yieldThenRun(() => values.get(key) ?? null), - set: (key: string, value: string) => yieldThenRun(() => { values.set(key, value); }), - }, - }); - const [first, second] = await Promise.all([ - claimPrActuationLock(env, "owner/act-repo", 7), - claimPrActuationLock(env, "owner/act-repo", 7), - ]); - expect([first.acquired, second.acquired]).toEqual([true, true]); - }); - - it("REGRESSION (#2129/#2135): a stale actuation-lock holder's release does not delete a successor's live lock", async () => { - // The exact race the ownership-token scheme exists to close: holder A's claim TTL lapses (or its finally - // block simply runs late), a NEW holder B claims the same key in the meantime, and then A's release finally - // runs. A blind del() would delete B's still-live lock; releaseIfValue only deletes when the caller's OWN - // token still matches what's stored, so A's late release is a safe no-op against B's key. - const env = createTestEnv({}); - const staleHolder = await claimPrActuationLock(env, "owner/act-repo", 7); - expect(staleHolder.acquired).toBe(true); - expect(staleHolder.ownerToken).toBeTruthy(); - // Simulate B's claim landing in the same key slot after A's token would have expired. - await env.SELFHOST_TRANSIENT_CACHE!.set!("pr-actuation-lock:owner/act-repo#7", "successor-token", 600); - await releasePrActuationLock(env, "owner/act-repo", 7, staleHolder.ownerToken); - expect(await env.SELFHOST_TRANSIENT_CACHE!.get!("pr-actuation-lock:owner/act-repo#7")).toBe("successor-token"); - // B's own release, with the matching token, does free the key. - await releasePrActuationLock(env, "owner/act-repo", 7, "successor-token"); - expect(await env.SELFHOST_TRANSIENT_CACHE!.get!("pr-actuation-lock:owner/act-repo#7")).toBeNull(); - }); - - it("releaseAiReviewLock and releasePrActuationLock are no-ops when ownerToken is null (nothing was actually claimed)", async () => { - const env = createTestEnv({}); - const calls: string[] = []; - env.SELFHOST_TRANSIENT_CACHE = { - get: async () => null, - set: async () => undefined, - releaseIfValue: async () => { calls.push("releaseIfValue"); return true; }, - }; - await releasePrActuationLock(env, "owner/act-repo", 7, null); - await releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", null); - expect(calls).toEqual([]); // a null token means nothing was claimed, so release must never touch the cache - }); - - it("REGRESSION: stale AI-review-lock holder releaseIfValue does not delete a successor's live lock", async () => { - const env = createTestEnv({}); - const staleHolder = await claimAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block"); - expect(staleHolder.acquired).toBe(true); - expect(staleHolder.ownerToken).toBeTruthy(); - await env.SELFHOST_TRANSIENT_CACHE!.set!("ai-review-lock:owner/agent-repo#7@sha1:block", "successor-token", 1800); - await releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", staleHolder.ownerToken); - expect(await env.SELFHOST_TRANSIENT_CACHE!.get!("ai-review-lock:owner/agent-repo#7@sha1:block")).toBe("successor-token"); - await releaseAiReviewLock(env, "owner/agent-repo", 7, "sha1", "block", "successor-token"); - expect(await env.SELFHOST_TRANSIENT_CACHE!.get!("ai-review-lock:owner/agent-repo#7@sha1:block")).toBeNull(); - }); - - it("claimPrActuationLock fails open without exclusivity when claim() is present but releaseIfValue is absent (#3153)", async () => { - let claimed = false; - const store = new Map(); - const env = createTestEnv({ - SELFHOST_TRANSIENT_CACHE: { - get: async (key: string) => store.get(key) ?? null, - set: async (key: string, value: string) => { store.set(key, value); }, - claim: async (key: string, value: string) => { - claimed = true; - if (store.has(key)) return false; - store.set(key, value); - return true; - }, - }, - }); - const lock = await claimPrActuationLock(env, "owner/act-repo", 7); - expect(lock.acquired).toBe(true); - expect(lock.ownerToken).toBeNull(); - expect(claimed).toBe(false); - expect(store.size).toBe(0); - }); - - it("INVARIANT (#2129 per-PR lock): a maintenance pass defers when another pass already holds the PR's lock", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); - let mergeCalls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.includes("/pulls/7/merge")) { - mergeCalls += 1; - return new Response(null, { status: 204 }); - } - if (url.includes("/pulls/7/reviews") && init?.method === "POST") return Response.json({ id: 1 }); - if (url.includes("/pulls/7/reviews")) return Response.json([]); - // Only the bare PR resource (no sub-path) — the more specific checks above already claimed - // /pulls/7/files, /pulls/7/merge, and /pulls/7/reviews. - if (/\/pulls\/7(\?|$)/.test(url)) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/commits/a7/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - if (url.includes(".gittensory.yml")) return new Response("Not Found", { status: 404 }); - if (url.endsWith("/check-runs") && init?.method === "POST") return Response.json({ id: 1 }); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.endsWith("/graphql")) return Response.json({ data: {} }); - return Response.json({}); - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - // Simulate a webhook pass already in-flight for this exact PR — a github-webhook:pr-refresh job's coalesce - // key never matches agent-regate-pr's, so the two would never dedup against each other pre-#2129; the - // shared per-PR actuation lock is what makes a second, independently-triggered pass defer instead of racing - // it. Pre-claims the SAME pr-actuation-lock key the draft-dodge/reopen-reclose paths use (#2129/#2135, - // review round 4) — one shared namespace, not a maintenance-only lock. - await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:owner/agent-repo#7", "1", 60); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "race-sweep", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); - - // The held lock made this pass skip its plan-and-execute critical section entirely — no mutation attempted. - expect(mergeCalls).toBe(0); - const actionAudits = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'agent.action.%'").first<{ n: number }>(); - expect(actionAudits?.n).toBe(0); - }); - - it("the sweep stamps the marker INLINE when the repo has no installation (audit-only, still converges) (#audit-sweep-fanout)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - // Configured but NOT installed (no installationId) — there is no installation to re-review with. - await upsertRepositoryFromGitHub(env, { name: "no-install", full_name: "owner/no-install", private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/no-install", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/no-install", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/no-install" }); - - expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([]); // no installation → no per-PR fan-out - const after = await env.DB.prepare("select last_regated_at from pull_requests where repo_full_name = ? and number = 7").bind("owner/no-install").first<{ last_regated_at: string | null }>(); - expect(typeof after?.last_regated_at).toBe("string"); // stamped inline so the sweep still advances - }); - - it("the sweep swallows a failing dispatch-time stamp on a no-installation repo and still completes (#audit-sweep-fanout)", async () => { - const env = createTestEnv({ JOBS: { async send() {} } as unknown as Queue }); - await upsertRepositoryFromGitHub(env, { name: "no-install", full_name: "owner/no-install", private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/no-install", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/no-install", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - const stamp = vi.spyOn(repositoriesModule, "markPullRequestsRegated").mockRejectedValueOnce(new Error("D1 write error")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/no-install" }); - - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); // the dispatch-time stamp failure is swallowed; the sweep still records its verdict - expect(errors.mock.calls.some((call) => String(call[0]).includes("sweep_mark_regated_failed"))).toBe(true); - stamp.mockRestore(); - errors.mockRestore(); - }); - - it("REGRESSION: the sweep DEFERS (re-queues, no fan-out) when the shared REST budget is below the maintenance floor (#audit-rate-headroom)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9200, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9200); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - // Low REST budget (10 ≤ 150 maintenance floor) with a future reset → maintenance must yield. Scoped to this - // repo's own installation bucket (#audit-rate-scoping) — the sweep now checks that bucket specifically. - await repositoriesModule.recordGitHubRateLimitObservation(env, { repoFullName: "owner/agent-repo", admissionKey: "installation:9200", resource: "rest", path: "/x", statusCode: 200, limitValue: 5000, remaining: 10, resetAt: "2026-05-28T02:30:00.000Z", observedAt: "2026-05-28T02:00:00.000Z" }); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([]); // no fan-out while deferred - expect(sent.some((m) => m.type === "agent-regate-sweep" && m.repoFullName === "owner/agent-repo")).toBe(true); // re-queued - const audit = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ outcome: string; metadata_json: string }>(); - expect(audit?.outcome).toBe("queued"); - expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ deferred: true }); - }); - - it("REGRESSION: a scheduled repo sweep does not fan out more per-PR regates while prior regate work is queued", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - snapshot: async () => ({ - totals: { pending: 1, processing: 0, dead: 0, due: 1 }, - byType: [{ type: "agent-regate-pr", status: "pending", count: 1, due: 1 }], - }), - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9201, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9201); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); - await upsertCheckSummary(env, { - id: "gate-backlog-7", - repoFullName: "owner/agent-repo", - pullNumber: 7, - headSha: "a7", - name: "Gittensory Orb Review Agent", - status: "completed", - conclusion: "success", - payload: {}, - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - const getRepo = vi.spyOn(repositoriesModule, "getRepository"); - const listOpen = vi.spyOn(repositoriesModule, "listOpenPullRequests"); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([]); - expect(getRepo).toHaveBeenCalledWith(env, "owner/agent-repo"); - expect(listOpen).toHaveBeenCalledWith(env, "owner/agent-repo"); - const audit = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.regate").first<{ outcome: string; metadata_json: string }>(); - expect(audit?.outcome).toBe("queued"); - expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ deferred: true, regateBacklog: 1 }); - }); - - it("REGRESSION: a scheduled repo sweep ignores sweep rows when deciding per-PR regate backlog", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - snapshot: async () => ({ - totals: { pending: 0, processing: 1, dead: 0, due: 0 }, - byType: [{ type: "agent-regate-sweep", status: "processing", count: 1, due: 0 }], - }), - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9203, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9203); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 9, title: "PR9", state: "open", user: { login: "c" }, head: { sha: "a9" }, labels: [], body: "" }); - // Published at the current head so this is an ORDINARY (non-priority-repair) candidate -- this test is about - // backlog-row-type filtering, not the priority-repair "regate-repair:" tagging (covered separately above). - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 9, "a9"); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([ - expect.objectContaining({ - type: "agent-regate-pr", - deliveryId: "regate-sweep:owner/agent-repo#9", - repoFullName: "owner/agent-repo", - prNumber: 9, - installationId: 9203, - }), - ]); - }); - - it("INVARIANT: a scheduled repo sweep does not require queue introspection", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - JOBS: { - async send(m: import("../../src/types").JobMessage) { - sent.push(m); - }, - snapshot: async () => { - throw new Error("snapshot unavailable"); - }, - } as unknown as Queue, - }); - await upsertInstallation(env, { action: "created", installation: { id: 9202, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9202); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 8, title: "PR8", state: "open", user: { login: "c" }, head: { sha: "a8" }, labels: [], body: "" }); - // Published at the current head so this is an ORDINARY (non-priority-repair) candidate -- this test is about - // queue-introspection independence, not the priority-repair "regate-repair:" tagging (covered separately above). - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 8, "a8"); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - await processJob(env, { type: "agent-regate-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([ - { - type: "agent-regate-pr", - deliveryId: "regate-sweep:owner/agent-repo#8", - repoFullName: "owner/agent-repo", - prNumber: 8, - installationId: 9202, - }, - ]); - }); - - it("REGRESSION: a per-PR re-gate job DEFERS (re-queues, no re-review/stamp) when the REST budget is below the maintenance floor (#audit-rate-headroom)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - // Scoped to this job's own installation bucket (#audit-rate-scoping) — installationId 9200 below. - await repositoriesModule.recordGitHubRateLimitObservation(env, { repoFullName: "owner/agent-repo", admissionKey: "installation:9200", resource: "rest", path: "/x", statusCode: 200, limitValue: 5000, remaining: 10, resetAt: "2026-05-28T02:30:00.000Z", observedAt: "2026-05-28T02:00:00.000Z" }); - const stamp = vi.spyOn(repositoriesModule, "markPullRequestsRegated"); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "regate-sweep:owner/agent-repo#7", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9200 }); - - expect(sent.filter((m) => m.type === "agent-regate-pr")).toHaveLength(1); // re-queued for after the reset - expect(stamp).not.toHaveBeenCalled(); // the per-PR job NEVER stamps the convergence marker — the sweep already did, at dispatch - stamp.mockRestore(); - }); - - it("REGRESSION: a 'regate-sweep:' per-PR job DEFERS at the maintenance floor even with headroom above the lower live floor (#selfhost-queue-liveness)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - // 100 remaining sits BELOW the 150 maintenance floor but ABOVE the 75 live floor -- isScheduledRegateSweepJob - // must route this "regate-sweep:"-prefixed job to the higher (150) floor, so it still defers here. Scoped to - // this job's own installation bucket (#audit-rate-scoping) — installationId 9200 below. - await repositoriesModule.recordGitHubRateLimitObservation(env, { repoFullName: "owner/agent-repo", admissionKey: "installation:9200", resource: "rest", path: "/x", statusCode: 200, limitValue: 5000, remaining: 100, resetAt: "2026-05-28T02:30:00.000Z", observedAt: "2026-05-28T02:00:00.000Z" }); - const stamp = vi.spyOn(repositoriesModule, "markPullRequestsRegated"); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "regate-sweep:owner/agent-repo#7", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9200 }); - - expect(sent.filter((m) => m.type === "agent-regate-pr")).toHaveLength(1); // re-queued for after the reset - expect(stamp).not.toHaveBeenCalled(); - stamp.mockRestore(); - }); - - it("REGRESSION: a non-'regate-sweep:' per-PR job (current-head trigger) does NOT defer at the maintenance floor, only at the lower live floor (#selfhost-queue-liveness)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - // Same 100-remaining observation as the sibling "regate-sweep:" test above (scoped to this job's own - // installation:9200 bucket, #audit-rate-scoping), but this deliveryId does NOT carry the "regate-sweep:" - // prefix (e.g. a repair-priority fan-out, or a real webhook-triggered re-review), so isScheduledRegateSweepJob - // is false and shouldWaitForGitHubRateLimit is called with the lower 75 floor: 100 > 75, so this job proceeds - // instead of deferring. - await repositoriesModule.recordGitHubRateLimitObservation(env, { repoFullName: "owner/agent-repo", admissionKey: "installation:9200", resource: "rest", path: "/x", statusCode: 200, limitValue: 5000, remaining: 100, resetAt: "2026-05-28T02:30:00.000Z", observedAt: "2026-05-28T02:00:00.000Z" }); - - // No stored PR row for prNumber 7 -- reReviewStoredPullRequest reaches its `getPullRequest` read (proving the - // rate-limit gate did not short-circuit it) and then returns immediately with no re-enqueue, since there is - // nothing to review. A deferral would instead re-enqueue this exact job (asserted absent below). - await processJob(env, { type: "agent-regate-pr", deliveryId: "regate-repair:owner/agent-repo#7", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9200 }); - - expect(sent.filter((m) => m.type === "agent-regate-pr")).toEqual([]); // proceeded — no rate-limit re-enqueue - }); - - it("routes repo-scoped backfill jobs into resumable segment and detail processors", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(message: import("../../src/types").JobMessage) { - sent.push(message); - }, - } as unknown as Queue, - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false } }, - { kind: "raw-github", url: "fixture://registry" }, - "2026-05-25T00:00:00.000Z", - ), - ); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.github.com/graphql") { - return Response.json({ - data: { - rateLimit: { remaining: 4999, resetAt: "2026-05-25T01:00:00.000Z" }, - repository: { - issues: { totalCount: 0 }, - openPullRequests: { totalCount: 0 }, - mergedPullRequests: { totalCount: 0 }, - closedPullRequests: { totalCount: 0 }, - labels: { totalCount: 0 }, - }, - }, - }); - } - if (url.includes("/issues?") || url.includes("/labels?") || url.includes("/pulls?")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { type: "backfill-registered-repos", requestedBy: "api", repoFullName: "JSONbored/gittensory" }); - await processJob(env, { type: "backfill-repo-segment", requestedBy: "api", repoFullName: "JSONbored/gittensory", segment: "open_issues" }); - await processJob(env, { type: "backfill-pr-details", requestedBy: "api", repoFullName: "JSONbored/gittensory" }); - - expect(sent).toEqual(expect.arrayContaining([expect.objectContaining({ type: "backfill-repo-segment", repoFullName: "JSONbored/gittensory" })])); - expect(await listRepoSyncStates(env)).toEqual(expect.arrayContaining([expect.objectContaining({ repoFullName: "JSONbored/gittensory" })])); - }); - - it("covers optional queue payload branches for fanout, segment, and detail jobs", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITHUB_PUBLIC_TOKEN: "public-token", - JOBS: { - async send(message: import("../../src/types").JobMessage) { - sent.push(message); - }, - } as unknown as Queue, - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { - "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false }, - "we-promise/sure": { emission_share: 0.02, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false }, - }, - { kind: "raw-github", url: "fixture://registry" }, - "2026-05-25T00:00:00.000Z", - ), - ); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.github.com/graphql") { - return Response.json({ - data: { - rateLimit: { remaining: 4999, resetAt: "2026-05-25T01:00:00.000Z" }, - repository: { - issues: { totalCount: 0 }, - openPullRequests: { totalCount: 0 }, - mergedPullRequests: { totalCount: 0 }, - closedPullRequests: { totalCount: 0 }, - labels: { totalCount: 0 }, - }, - }, - }); - } - if (url.includes("/labels?") || url.includes("/pulls?") || url.includes("/issues?")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { type: "backfill-registered-repos", requestedBy: "api" }); - await processJob(env, { type: "backfill-repo-segment", requestedBy: "api", repoFullName: "JSONbored/gittensory", segment: "labels", mode: "resume", cursor: "2", force: true }); - await processJob(env, { type: "backfill-pr-details", requestedBy: "api", repoFullName: "JSONbored/gittensory", mode: "resume", cursor: 2 }); - - expect(sent).toEqual(expect.arrayContaining([expect.objectContaining({ type: "backfill-registered-repos", repoFullName: "JSONbored/gittensory" })])); - }); - - it("marks installation health from queued installation metadata", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", contents: "write", pull_requests: "write", issues: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }], - }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, 123); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.endsWith("/app/installations/123")) { - return Response.json({ - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { type: "refresh-installation-health", requestedBy: "test" }); - expect(await listInstallationHealth(env)).toMatchObject([{ status: "healthy", registeredInstalledCount: 1 }]); - }); - - it("syncs repositories added to and removed from an existing installation", async () => { - const env = createTestEnv(); - const installation = { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }; - await upsertInstallation(env, { - installation: { - ...installation, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "installation-repo-added", - eventName: "installation_repositories", - payload: { - action: "added", - installation: { id: 123 }, - repositories_added: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }, - }); - - expect(await getRepository(env, "JSONbored/gittensory")).toMatchObject({ isInstalled: true, installationId: 123 }); - expect(await getInstallation(env, 123)).toMatchObject({ - accountLogin: "JSONbored", - permissions: { metadata: "read", pull_requests: "read", issues: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "installation-repo-removed", - eventName: "installation_repositories", - payload: { - action: "removed", - installation: { id: 123 }, - repositories_removed: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }, - }); - - expect(await getRepository(env, "JSONbored/gittensory")).toMatchObject({ isInstalled: false, installationId: null }); - expect(await listProductUsageEvents(env, { limit: 10 })).toEqual( - expect.arrayContaining([ - expect.objectContaining({ eventName: "github_installation_repository_added", repoFullName: "/gittensory" }), - expect.objectContaining({ eventName: "github_installation_repository_removed", repoFullName: "/gittensory" }), - ]), - ); - }); - - it("does not record phantom telemetry when installation-created has no repositories (#installation-created-fallback)", async () => { - const env = createTestEnv(); - - // Case 1: neither repositories nor repository.full_name — must produce zero events (was [undefined]) - await processJob(env, { - type: "github-webhook", - deliveryId: "install-no-repos", - eventName: "installation", - payload: { - action: "created", - installation: { id: 900, account: { login: "empty-org", id: 99, type: "Organization" } }, - }, - }); - const eventsAfterEmpty = await listProductUsageEvents(env, { limit: 50 }); - expect(eventsAfterEmpty.filter((e) => e.eventName === "github_installation_created")).toHaveLength(0); - - // Case 2: repository fallback (no repositories array) — must produce exactly one event with consistent metadata - await processJob(env, { - type: "github-webhook", - deliveryId: "install-single-repo-fallback", - eventName: "installation", - payload: { - action: "created", - installation: { id: 901, account: { login: "single-org", id: 100, type: "Organization" } }, - repository: { name: "my-repo", full_name: "single-org/my-repo", private: false, owner: { login: "single-org" } }, - }, - }); - const eventsAfterSingle = await listProductUsageEvents(env, { limit: 50 }); - const createdEvents = eventsAfterSingle.filter((e) => e.eventName === "github_installation_created"); - expect(createdEvents).toHaveLength(1); - expect(createdEvents[0]).toMatchObject({ - eventName: "github_installation_created", - repoFullName: "/my-repo", - metadata: expect.objectContaining({ action: "created", repoCount: 1, truncatedRepos: 0 }), - }); - }); - - it("REGRESSION: installation-created telemetry falls back to repoFullName as the targetKey when the payload carries no installation.id", async () => { - const env = createTestEnv(); - // `handleInstallationCreatedWebhookEvent`'s own guard is `eventName === "installation" && action === "created"` - // -- unlike the sibling installation_repositories handler, it does NOT also require `installation.id`, so a - // malformed/partial delivery (no `installation` object at all) still enters the block. The per-repo - // `targetKey: payload.installation?.id ? \`installation:${id}\` : repoFullName` ternary must then take its - // `repoFullName` fallback arm instead of throwing or omitting the field. - await processJob(env, { - type: "github-webhook", - deliveryId: "install-created-no-installation-id", - eventName: "installation", - payload: { - action: "created", - repository: { name: "my-repo", full_name: "no-installation-org/my-repo", private: false, owner: { login: "no-installation-org" } }, - }, - }); - const events = await listProductUsageEvents(env, { limit: 50 }); - const created = events.filter((e) => e.eventName === "github_installation_created"); - expect(created).toHaveLength(1); - // No installation/sender on the payload -> installationActor is undefined -> no actor redaction applies, so - // both fields surface the real (unredacted) value here. - expect(created[0]).toMatchObject({ - eventName: "github_installation_created", - repoFullName: "no-installation-org/my-repo", - targetKey: "no-installation-org/my-repo", - }); - }); - - it("REGRESSION: a deployment_status webhook for an allowlisted repo re-reviews the correlated PR and short-circuits before the other wake triggers", async () => { - const env = createTestEnv({ GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory" }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - // Deliberately no stored PR #4242 -- reReviewStoredPullRequest's own `if (!pr || pr.state !== "open") return;` - // no-ops immediately, so this test stays focused on maybeCaptureOnDeploymentStatus's early-return contract - // (processGitHubWebhook must `return` right after it, never falling through to the other wake-trigger checks) - // without needing to mock the full re-review pipeline. - await processJob(env, { - type: "github-webhook", - deliveryId: "deployment-status-4242", - eventName: "deployment_status", - payload: { - installation: { id: 123 }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - deployment_status: { state: "success", environment_url: "https://preview.example.test" }, - deployment: { sha: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", ref: "feature", payload: JSON.stringify({ pr: 4242 }) }, - }, - } as never); - const stored = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("deployment-status-4242").first<{ status: string }>(); - expect(stored?.status).toBe("processed"); - }); - - it("publishes an opt-in gate without comment output, blocking a non-confirmed author normally (#gate-nonconfirmed)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - requireLinkedIssue: true, - }); - const calls = { minerList: 0, gateChecks: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([]); - } - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && (init?.method ?? "GET") === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string; status?: string; conclusion?: string; output?: { title?: string } }; - expect(body).toMatchObject({ name: "Gittensory Orb Review Agent", status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); - expect(body.conclusion).toBeUndefined(); - calls.gateChecks += 1; - return Response.json({ id: 900 }, { status: 201 }); - } - if (url.includes("/check-runs/900") && (init?.method ?? "GET") === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string; status?: string; conclusion?: string; output?: { title?: string } }; - // Non-confirmed author + linked-issue block + no issue → gated normally → failure (#gate-nonconfirmed). - expect(body).toMatchObject({ name: "Gittensory Orb Review Agent", status: "completed", conclusion: "failure", output: { title: "Gittensory Orb Review Agent: No linked issue detected" } }); - calls.gateChecks += 1; - return Response.json({ id: 900, html_url: "https://github.com/checks/900" }); - } - return new Response("not found", { status: 404 }); - }); - - // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code). - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-only", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 42, title: "Gate without issue", state: "open", user: { login: "contributor" }, head: { sha: "gate123" }, labels: [], body: "No issue link." }, - }, - }); - - expect(calls).toEqual({ minerList: 1, gateChecks: 2 }); - const stored = await getPullRequest(env, "JSONbored/gittensory", 42); - expect(stored?.lastPublishedSurfaceSha).toBe("gate123"); - const published = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_published") - .first<{ metadata_json: string }>(); - expect(published?.metadata_json).toContain('"publishedOutputs":["gate_check_run"]'); - const summary = await env.DB.prepare("select name, status, conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") - .bind("JSONbored/gittensory", 42, "gate123") - .first<{ name: string; status: string; conclusion: string }>(); - expect(summary).toMatchObject({ - name: "Gittensory Orb Review Agent", - status: "completed", - conclusion: "failure", - }); - }); - - it("blocks under linkedIssueGateMode:block when the PR only cites an already-CLOSED issue (#unlinked-issue-guardrail-followup — the stale-link gaming case)", async () => { - // Before the fix, pr.linkedIssues.length > 0 alone satisfied this gate regardless of the cited issue's real - // state — a contributor could cite an already-closed (or fabricated) issue number to fake compliance. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - requireLinkedIssue: true, - }); - // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code) — mirrors the - // existing "publishes an opt-in gate..." test above, which needs the same manifest override for the raw - // DB setting to take effect as a live hard block. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/5") && !url.includes("/comments")) return Response.json({ number: 5, state: "closed", labels: [], assignees: [] }); - if (url.includes("/commits/gate124/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && (init?.method ?? "GET") === "POST") return Response.json({ id: 901 }, { status: 201 }); - if (url.includes("/check-runs/901") && (init?.method ?? "GET") === "PATCH") return Response.json({ id: 901, html_url: "https://github.com/checks/901" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-stale-link", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 43, title: "Fake compliance", state: "open", user: { login: "contributor" }, head: { sha: "gate124" }, labels: [], body: "Closes #5" }, - }, - }); - - const summary = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") - .bind("JSONbored/gittensory", 43, "gate124") - .first<{ conclusion: string }>(); - expect(summary?.conclusion).toBe("failure"); - }); - - it("does NOT block under linkedIssueGateMode:block when the cited issue is genuinely OPEN", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - requireLinkedIssue: true, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/5") && !url.includes("/comments")) return Response.json({ number: 5, state: "open", labels: [], assignees: [] }); - if (url.includes("/commits/gate125/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && (init?.method ?? "GET") === "POST") return Response.json({ id: 902 }, { status: 201 }); - if (url.includes("/check-runs/902") && (init?.method ?? "GET") === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")) as { conclusion?: string; output?: { title?: string } }; - expect(body.output?.title).not.toBe("Gittensory Orb Review Agent: No linked issue detected"); - return Response.json({ id: 902, html_url: "https://github.com/checks/902" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-open-link", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 44, title: "Real link", state: "open", user: { login: "contributor" }, head: { sha: "gate125" }, labels: [], body: "Closes #5" }, - }, - }); - - const summary = await env.DB.prepare("select conclusion from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") - .bind("JSONbored/gittensory", 44, "gate125") - .first<{ conclusion: string }>(); - expect(summary?.conclusion).not.toBe("failure"); - }); - - it("accepts PR-body validation evidence for configured manifest test expectations", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - manifestPolicyGateMode: "block", - requireLinkedIssue: false, - typeLabelsEnabled: false, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { testExpectations: ["Run npm run test:ci."] }); - await upsertPullRequestFile(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 43, - path: "src/feature.ts", - status: "modified", - additions: 1, - deletions: 0, - changes: 1, - payload: {}, - }); - - const gatePatches: Array> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-validation/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - if (url.includes("/check-runs/901") && method === "PATCH") { - gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); - return Response.json({ id: 901, html_url: "https://github.com/checks/901" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-validation-evidence", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 43, - title: "Validated change", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate-validation" }, - labels: [], - body: "Validated with npm run test:ci.", - }, - }, - }); - - expect(gatePatches).toHaveLength(1); - expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "success" }); - expect(JSON.stringify(gatePatches[0])).not.toContain("Configured validation evidence missing"); - expect(JSON.stringify(gatePatches[0])).not.toContain("manifest_missing_tests"); - }); - - // REGRESSION (#3304): a PR body that merely MENTIONS testing without affirming it was done ("No tests - // run.") must not satisfy a configured manifest test expectation on the live webhook gate path. - it("still flags manifest_missing_tests for a PR body that only claims tests were NOT run", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - manifestPolicyGateMode: "block", - requireLinkedIssue: false, - typeLabelsEnabled: false, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { testExpectations: ["Run npm run test:ci."] }); - await upsertPullRequestFile(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 44, - path: "src/feature.ts", - status: "modified", - additions: 1, - deletions: 0, - changes: 1, - payload: {}, - }); - - const gatePatches: Array> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-no-validation/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 902 }, { status: 201 }); - if (url.includes("/check-runs/902") && method === "PATCH") { - gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); - return Response.json({ id: 902, html_url: "https://github.com/checks/902" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-no-validation-evidence", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 44, - title: "Unvalidated change", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate-no-validation" }, - labels: [], - body: "No tests run.", - }, - }, - }); - - expect(gatePatches).toHaveLength(1); - expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "failure" }); - expect(JSON.stringify(gatePatches[0])).toContain("Configured validation evidence missing"); - }); - - // #4607 (maybeApplyManifestPolicyGate extraction): buildFocusManifestGuidance can produce findings whose - // code is NOT one of the three enforceable manifest-policy codes (manifest_blocked_path / - // manifest_linked_issue_required / manifest_missing_tests) -- e.g. manifest_off_focus, when wantedPaths is - // configured and no changed path matches it. Those non-enforceable findings must be filtered out before - // ever reaching the advisory/gate, never published alongside an enforceable one from the same pass. - it("filters out a non-enforceable manifest finding (manifest_off_focus) while still surfacing an enforceable one", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - manifestPolicyGateMode: "block", - requireLinkedIssue: false, - typeLabelsEnabled: false, - }); - // wantedPaths configured + a changed file outside it produces manifest_off_focus (NOT one of the three - // enforceable codes); testExpectations configured + no evidence produces manifest_missing_tests (IS - // enforceable) -- so this single pass yields one filtered finding and one published finding. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { - wantedPaths: ["docs/"], - testExpectations: ["Run npm run test:ci."], - }); - await upsertPullRequestFile(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 45, - path: "src/feature.ts", - status: "modified", - additions: 1, - deletions: 0, - changes: 1, - payload: {}, - }); - - const gatePatches: Array> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-off-focus/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 903 }, { status: 201 }); - if (url.includes("/check-runs/903") && method === "PATCH") { - gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); - return Response.json({ id: 903, html_url: "https://github.com/checks/903" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-off-focus-filtered", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 45, - title: "Out of focus change", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate-off-focus" }, - labels: [], - body: "No tests run.", - }, - }, - }); - - expect(gatePatches).toHaveLength(1); - // The enforceable finding (manifest_missing_tests) is published... - expect(JSON.stringify(gatePatches[0])).toContain("Configured validation evidence missing"); - // ...but the non-enforceable finding (manifest_off_focus) is filtered out before it ever reaches the advisory. - expect(JSON.stringify(gatePatches[0])).not.toContain("Change is outside maintainer-wanted areas"); - expect(JSON.stringify(gatePatches[0])).not.toContain("manifest_off_focus"); - }); - - // REGRESSION (#3304): a PR with no body at all (GitHub sends `body: null` for an empty description) must - // fall back to treating validation evidence as absent, not throw or silently pass the manifest gate. - it("still flags manifest_missing_tests for a PR with a null body", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - manifestPolicyGateMode: "block", - requireLinkedIssue: false, - typeLabelsEnabled: false, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { testExpectations: ["Run npm run test:ci."] }); - await upsertPullRequestFile(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 45, - path: "src/feature.ts", - status: "modified", - additions: 1, - deletions: 0, - changes: 1, - payload: {}, - }); - - const gatePatches: Array> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-null-body/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 903 }, { status: 201 }); - if (url.includes("/check-runs/903") && method === "PATCH") { - gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); - return Response.json({ id: 903, html_url: "https://github.com/checks/903" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-null-body-evidence", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 45, - title: "No-description change", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate-null-body" }, - labels: [], - body: null, - }, - }, - }); - - expect(gatePatches).toHaveLength(1); - expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "failure" }); - expect(JSON.stringify(gatePatches[0])).toContain("Configured validation evidence missing"); - }); - - // REGRESSION (#4719 gate-review finding): passedValidationCount previously came ONLY from a PR-body - // prose match (hasValidationNote), with zero connection to the PR's actual CI results -- a fully green - // PR whose body simply doesn't happen to use a "tested"/"validated" word still tripped - // manifest_missing_tests. A fully-green live CI rollup must now ALSO count as validation evidence. - it("treats a fully-green live CI rollup as validation evidence even with no body validation note (#4719)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - manifestPolicyGateMode: "block", - requireLinkedIssue: false, - typeLabelsEnabled: false, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { testExpectations: ["Run npm run test:ci."] }); - await upsertPullRequestFile(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 46, - path: "src/feature.ts", - status: "modified", - additions: 1, - deletions: 0, - changes: 1, - payload: {}, - }); - - const gatePatches: Array> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // A single completed+successful, first-party check-run with no failing/pending statuses -- the - // live CI aggregate resolves this to ciState: "passed". - if (url.includes("/commits/gate-ci-green/check-runs")) { - return Response.json({ total_count: 1, check_runs: [{ name: "build", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - } - if (url.includes("/commits/gate-ci-green/status")) return Response.json({ statuses: [] }); - if (url.includes("/commits/gate-ci-green/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 904 }, { status: 201 }); - if (url.includes("/check-runs/904") && method === "PATCH") { - gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); - return Response.json({ id: 904, html_url: "https://github.com/checks/904" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-ci-green-evidence", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 46, - title: "CI-green change with a plain description", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate-ci-green" }, - labels: [], - body: "Fixes the checkout retry bug.", - }, - }, - }); - - expect(gatePatches).toHaveLength(1); - expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "success" }); - expect(JSON.stringify(gatePatches[0])).not.toContain("manifest_missing_tests"); - expect(JSON.stringify(gatePatches[0])).not.toContain("Configured validation evidence missing"); - }); - - // REGRESSION: review.auto_review.ignore_authors is only an AI/public-output skip. It must not - // suppress deterministic manifest policy blockers or the e2e-test-generation trigger that reads them. - it("still flags manifest_missing_tests for an ignored bot author without validation evidence", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - manifestPolicyGateMode: "block", - requireLinkedIssue: false, - typeLabelsEnabled: false, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { - testExpectations: ["Run npm run test:ci."], - review: { auto_review: { ignore_authors: ["*[bot]"] } }, - }); - await upsertPullRequestFile(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 47, - path: "README.md", - status: "modified", - additions: 1, - deletions: 1, - changes: 2, - payload: {}, - }); - - const gatePatches: Array> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-ignored-bot-blocked/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 905 }, { status: 201 }); - if (url.includes("/check-runs/905") && method === "PATCH") { - gatePatches.push(JSON.parse(String(init?.body ?? "{}")) as Record); - return Response.json({ id: 905, html_url: "https://github.com/checks/905" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-ignored-bot-blocked-evidence", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 47, - title: "Update README", - state: "open", - user: { login: "github-actions[bot]" }, - head: { sha: "gate-ignored-bot-blocked" }, - labels: [], - body: "Auto-generated by a workflow.", - }, - }, - }); - - expect(gatePatches).toHaveLength(1); - expect(gatePatches[0]).toMatchObject({ status: "completed", conclusion: "failure" }); - expect(JSON.stringify(gatePatches[0])).toContain("Configured validation evidence missing"); - }); - - it("stamps a gate-only surface even when local Gate check-summary persistence fails", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const realPrepare = env.DB.prepare.bind(env.DB); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - env.DB.prepare = ((sql: string) => { - if (/insert\s+into\s+["`]?check_summaries["`]?/i.test(sql)) throw new Error("summary write failed"); - return realPrepare(sql); - }) as typeof env.DB.prepare; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-summary-fails/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 975 }, { status: 201 }); - if (url.includes("/check-runs/975") && method === "PATCH") return Response.json({ id: 975, html_url: "https://github.com/checks/975" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-summary-fails", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 85, title: "Gate summary fails", state: "open", user: { login: "contributor" }, head: { sha: "gate-summary-fails" }, labels: [], body: "No issue link." }, - }, - }); - - const stored = await getPullRequest(env, "JSONbored/gittensory", 85); - expect(stored?.lastPublishedSurfaceSha).toBe("gate-summary-fails"); - expect(errors.mock.calls.some((call) => String(call[0]).includes("gate_check_summary_upsert_failed"))).toBe(true); - errors.mockRestore(); - }); - - it("finalizes a permission-missing gate check through the neutral fallback before stamping the surface", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const realPrepare = env.DB.prepare.bind(env.DB); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - env.DB.prepare = ((sql: string) => { - if (/insert\s+into\s+["`]?check_summaries["`]?/i.test(sql)) throw new Error("summary write failed"); - return realPrepare(sql); - }) as typeof env.DB.prepare; - let patches = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-permission-fallback/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 976 }, { status: 201 }); - if (url.includes("/check-runs/976") && method === "PATCH") { - patches += 1; - if (patches === 1) - return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); - return Response.json({ id: 976, html_url: "https://github.com/checks/976" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-permission-fallback", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 86, title: "Gate permission fallback", state: "open", user: { login: "contributor" }, head: { sha: "gate-permission-fallback" }, labels: [], body: "No issue link." }, - }, - }); - - expect(patches).toBe(2); - const stored = await getPullRequest(env, "JSONbored/gittensory", 86); - expect(stored?.lastPublishedSurfaceSha).toBe("gate-permission-fallback"); - expect(errors.mock.calls.some((call) => String(call[0]).includes("gate_check_permission_missing"))).toBe(true); - expect(errors.mock.calls.some((call) => String(call[0]).includes("gate_check_summary_upsert_failed"))).toBe(true); - errors.mockRestore(); - }); - - it("does not stamp a permission-missing gate check when the neutral fallback cannot publish", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - let patches = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-permission-fallback-fails/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 977 }, { status: 201 }); - if (url.includes("/check-runs/977") && method === "PATCH") { - patches += 1; - if (patches === 1) - return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); - return new Response("fallback update failed", { status: 500 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-permission-fallback-fails", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 87, title: "Gate permission fallback fails", state: "open", user: { login: "contributor" }, head: { sha: "gate-permission-fallback-fails" }, labels: [], body: "No issue link." }, - }, - }); - - expect(patches).toBe(2); - const stored = await getPullRequest(env, "JSONbored/gittensory", 87); - expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); - const incomplete = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_incomplete") - .first<{ metadata_json: string }>(); - expect(incomplete?.metadata_json).toContain('"publishedOutputs":[]'); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_published") - .all(); - expect(published.results).toEqual([]); - }); - - it("suppresses public review output when the live PR head changed before publish", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - aiReviewMode: "off", - }); - let commentPosts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/stale.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const stale = true;" }]); - if (/\/pulls\/55(?:\?|$)/.test(url)) return Response.json({ number: 55, title: "Stale before publish", state: "open", user: { login: "contributor" }, head: { sha: "newsha" }, labels: [], body: "Fixes #1" }); - if (url.includes("/issues/55/comments") && method === "POST") { - commentPosts += 1; - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes("/issues/55/comments") && method === "GET") return Response.json([]); - return new Response("not found", { status: 404 }); - }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValue({ - status: "stale", - reason: "head_changed", - expectedHeadSha: "oldsha", - liveHeadSha: "newsha", - liveState: "open", - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "stale-before-public-output", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Stale before publish", state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(commentPosts).toBe(0); - const stale = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_review_stale") - .first<{ detail: string; metadata_json: string }>(); - expect(stale?.detail).toContain("PR head changed from oldsha to newsha"); - expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ - phase: "pre_public_output", - reason: "head_changed", - expectedHeadSha: "oldsha", - liveHeadSha: "newsha", - }); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_published") - .all(); - expect(published.results).toEqual([]); - }); - - it("retries unavailable live PR freshness while suppressing terminal stale review output", async () => { - const cases = [ - { - pullNumber: 59, - deliveryId: "unavailable-before-public-output", - title: "Unavailable before publish", - freshness: classifyPullRequestFreshness(undefined, "oldsha", { - unavailableSource: "pull_request_fetch", - unavailableDetail: "GitHub API failed for JSONbored/gittensory/pulls/59 (503)", - }), - expectRetry: true, - expectedDetail: "live PR state could not be verified", - expectedMetadata: { - reason: "unavailable", - expectedHeadSha: "oldsha", - liveHeadSha: null, - liveState: null, - unavailableSource: "pull_request_fetch", - unavailableDetail: "GitHub API failed for JSONbored/gittensory/pulls/59 (503)", - }, - }, - { - pullNumber: 60, - deliveryId: "head-unresolved-before-public-output", - title: "Unresolved head before publish", - freshness: classifyPullRequestFreshness( - { - state: "open", - head: {}, - }, - "oldsha", - ), - expectRetry: false, - expectedDetail: "live PR head SHA could not be verified", - expectedMetadata: { - reason: "head_unresolved", - expectedHeadSha: "oldsha", - liveHeadSha: null, - liveState: "open", - }, - }, - { - pullNumber: 61, - deliveryId: "unavailable-no-detail-before-public-output", - title: "Unavailable before publish without detail", - freshness: classifyPullRequestFreshness(undefined, "oldsha"), - expectRetry: true, - expectedDetail: "live PR state could not be verified", - expectedMetadata: { - reason: "unavailable", - expectedHeadSha: "oldsha", - liveHeadSha: null, - liveState: null, - unavailableSource: "unknown", - unavailableDetail: null, - }, - }, - ] as const; - - for (const scenario of cases) { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - aiReviewMode: "off", - }); - let commentPosts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes(`/pulls/${scenario.pullNumber}/files`)) return Response.json([{ filename: "src/stale.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const stale = true;" }]); - if (url.includes(`/issues/${scenario.pullNumber}/comments`) && method === "POST") { - commentPosts += 1; - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes(`/issues/${scenario.pullNumber}/comments`) && method === "GET") return Response.json([]); - return new Response("not found", { status: 404 }); - }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValue(scenario.freshness); - - const job = processJob(env, { - type: "github-webhook", - deliveryId: scenario.deliveryId, - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: scenario.pullNumber, title: scenario.title, state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "Fixes #1" }, - }, - }); - if (scenario.expectRetry) await expect(job).rejects.toThrow("live PR state unavailable"); - else await expect(job).resolves.toBeUndefined(); - - expect(commentPosts).toBe(0); - const stale = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_review_stale") - .first<{ detail: string; metadata_json: string }>(); - expect(stale?.detail).toContain(scenario.expectedDetail); - expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ - phase: "pre_public_output", - ...scenario.expectedMetadata, - }); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_published") - .all(); - expect(published.results).toEqual([]); - } - }); - - it("suppresses public review output for no-head reviews when the live PR is closed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - aiReviewMode: "off", - }); - let commentPosts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/61/files")) return Response.json([{ filename: "src/no-head.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const noHead = true;" }]); - if (url.includes("/issues/61/comments") && method === "POST") { - commentPosts += 1; - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes("/issues/61/comments") && method === "GET") return Response.json([]); - return new Response("not found", { status: 404 }); - }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValue(classifyPullRequestFreshness({ state: "closed", head: {} }, null)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "no-head-closed-before-public-output", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 61, title: "No head before publish", state: "open", user: { login: "contributor" }, head: {}, labels: [], body: "Fixes #1" }, - }, - }); - - expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ expectedHeadSha: null })); - expect(commentPosts).toBe(0); - const stale = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_review_stale") - .first<{ detail: string; metadata_json: string }>(); - expect(stale?.detail).toContain("PR is no longer open"); - expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ - phase: "pre_public_output", - reason: "closed", - expectedHeadSha: null, - liveHeadSha: null, - liveState: "closed", - }); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_published") - .all(); - expect(published.results).toEqual([]); - }); - - it("still suppresses stale public output when the stale audit write fails", async () => { - const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; - let staleAuditWrites = 0; - const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { - if (event.eventType === "github_app.pr_review_stale") { - staleAuditWrites += 1; - throw new Error("D1 audit failed"); - } - await originalRecordAuditEvent(auditEnv, event); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - aiReviewMode: "off", - }); - let commentPosts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/57/files")) return Response.json([{ filename: "src/stale.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const stale = true;" }]); - if (/\/pulls\/57(?:\?|$)/.test(url)) return Response.json({ number: 57, title: "Stale audit failure", state: "open", user: { login: "contributor" }, head: { sha: "newsha" }, labels: [], body: "Fixes #1" }); - if (url.includes("/issues/57/comments") && method === "POST") { - commentPosts += 1; - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes("/issues/57/comments") && method === "GET") return Response.json([]); - return new Response("not found", { status: 404 }); - }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValue({ - status: "stale", - reason: "head_changed", - expectedHeadSha: "oldsha", - liveHeadSha: "newsha", - liveState: "open", - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "stale-audit-failure", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 57, title: "Stale audit failure", state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "Fixes #1" }, - }, - }); - } finally { - auditSpy.mockRestore(); - } - - expect(staleAuditWrites).toBe(1); - expect(commentPosts).toBe(0); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_published") - .all(); - expect(published.results).toEqual([]); - }); - - it("finalizes the pending gate as skipped when the PR head changes after review work", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - let livePullReads = 0; - const checkBodies: Array<{ status?: string; conclusion?: string; output?: { title?: string; summary?: string } }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url === "https://api.github.com/graphql") { - return Response.json({ data: { repository: { pullRequest: { reviewThreads: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } }); - } - if (url.includes("/pulls/56/files")) return Response.json([{ filename: "src/final.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const final = true;" }]); - if (/\/pulls\/56(?:\?|$)/.test(url)) { - livePullReads += 1; - return Response.json({ - number: 56, - title: "Stale after review", - state: "open", - user: { login: "contributor" }, - head: { sha: "newsha" }, - labels: [], - body: "No issue link.", - }); - } - if (url.includes("/commits/oldsha/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/oldsha/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - if (url.includes("/check-runs") && method === "POST") { - checkBodies.push(JSON.parse(String(init?.body ?? "{}"))); - return Response.json({ id: 906 }, { status: 201 }); - } - if (url.includes("/check-runs/906") && method === "PATCH") { - checkBodies.push(JSON.parse(String(init?.body ?? "{}"))); - return Response.json({ id: 906 }); - } - return new Response("not found", { status: 404 }); - }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValue({ - status: "stale", - reason: "head_changed", - expectedHeadSha: "oldsha", - liveHeadSha: "newsha", - liveState: "open", - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "stale-after-review", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 56, title: "Stale after review", state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "No issue link." }, - }, - }); - - expect(livePullReads).toBe(0); - expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ expectedHeadSha: "oldsha" })); - expect(checkBodies).toHaveLength(2); - expect(checkBodies[0]).toMatchObject({ status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); - expect(checkBodies[1]).toMatchObject({ - status: "completed", - conclusion: "skipped", - output: { - title: "Gittensory Orb Review Agent skipped", - summary: "PR head changed from oldsha to newsha", - }, - }); - const stale = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_review_stale") - .first<{ metadata_json: string }>(); - expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ phase: "final_publish", reason: "head_changed" }); - }); - - it("still suppresses stale final output when the skipped gate check update fails", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - let patchAttempts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url === "https://api.github.com/graphql") { - return Response.json({ data: { repository: { pullRequest: { reviewThreads: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } } } }); - } - if (url.includes("/pulls/58/files")) return Response.json([{ filename: "src/final.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const final = true;" }]); - if (/\/pulls\/58(?:\?|$)/.test(url)) return Response.json({ number: 58, title: "Stale skip failure", state: "open", user: { login: "contributor" }, head: { sha: "newsha" }, labels: [], body: "No issue link." }); - if (url.includes("/commits/oldsha/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/oldsha/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 907 }, { status: 201 }); - if (url.includes("/check-runs/907") && method === "PATCH") { - patchAttempts += 1; - throw new Error("check-run update failed"); - } - return new Response("not found", { status: 404 }); - }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValue({ - status: "stale", - reason: "head_changed", - expectedHeadSha: "oldsha", - liveHeadSha: "newsha", - liveState: "open", - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "stale-skip-failure", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 58, title: "Stale skip failure", state: "open", user: { login: "contributor" }, head: { sha: "oldsha" }, labels: [], body: "No issue link." }, - }, - }); - - expect(patchAttempts).toBe(1); - const stale = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_review_stale") - .first<{ metadata_json: string }>(); - expect(JSON.parse(stale?.metadata_json ?? "{}")).toMatchObject({ phase: "final_publish", reason: "head_changed" }); - }); - - it("auto-maintain (#778): a blocking gate on an agent-configured repo records the changes-requested label, never a formal request_changes (dry-run)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", contents: "write", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - requireLinkedIssue: true, - autonomy: { review_state_label: "auto", request_changes: "auto" }, - agentDryRun: true, // dry-run → the actions are recorded but make no GitHub mutation - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - // .gittensory.yml authoritatively sets the linked-issue blocker to "block" (config-as-code, as in the gate tests above). - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/gate123/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "auto-maintain", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 42, title: "No issue", state: "open", user: { login: "contributor" }, head: { sha: "gate123" }, labels: [], body: "No issue link." }, - }, - }); - - const labelAudit = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.action.label").first<{ outcome: string; metadata_json: string }>(); - expect(labelAudit?.outcome).toBe("completed"); - expect(JSON.parse(labelAudit?.metadata_json ?? "{}")).toMatchObject({ mode: "dry_run", actionClass: "label" }); - // The bot NEVER posts a formal request_changes (a blocking review strands the PR). With close NOT at an acting - // level here, a blocking contributor PR is only labeled; with close acting it would be closed. No request_changes. - const rcAudit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.request_changes").first<{ outcome: string }>(); - expect(rcAudit).toBeFalsy(); - }); - - it("auto-maintain (#778): uses hard guardrails so guarded paths cannot be merged", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - manifestPolicyGateMode: "block", - autonomy: { merge: "auto", request_changes: "auto" }, - agentDryRun: true, - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { hardGuardrailGlobs: ["migrations/**"] } }); - await upsertPullRequestFile(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 48, - path: "migrations/0099_attacker.sql", - status: "modified", - additions: 1, - deletions: 0, - changes: 1, - payload: {}, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/gate123/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "auto-maintain-hard-guardrail", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 48, - title: "Blocked migration", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate123" }, - labels: [], - body: "Closes #1", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - - const mergeCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.merge").first<{ n: number }>(); - expect(mergeCount?.n).toBe(0); // the hard guardrail prevents the auto-merge (the key assertion) - // The bot never posts a formal request_changes. With close NOT at an acting level here, the blocked PR is - // simply not merged (no blocking review); with close acting it would be closed. - const rcAudit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.request_changes").first<{ outcome: string }>(); - expect(rcAudit).toBeFalsy(); - }); - - it("refreshes pull request files for path-gated pre-merge checks on synchronize (#review-pre-merge-checks)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - autonomy: { merge: "observe", request_changes: "observe" }, - slopGateMode: "off", - mergeReadinessGateMode: "off", - manifestPolicyGateMode: "off", - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { - review: { pre_merge_checks: [{ name: "Migration approval", require_label: "approved", when_paths: ["migrations/**"], enforce: true }] }, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 49, - title: "feat: add migration", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate125" }, - labels: [], - body: "Closes #1", - }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 49, path: "src/feature.ts", status: "modified", additions: 1, deletions: 0, changes: 1, payload: {} }); - - let pullFilesFetches = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/49/files")) { - pullFilesFetches += 1; - return Response.json([{ filename: "migrations/0099_security.sql", status: "added", additions: 3, deletions: 0, changes: 3 }]); - } - if (url.includes("/pulls/49/reviews")) return Response.json([]); - if (url.includes("/commits/gate125/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/gate125/status")) return Response.json({ statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "pre-merge-refresh-sync", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 49, - title: "feat: add migration", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate125" }, - labels: [], - body: "Closes #1", - mergeable_state: "clean", - }, - }, - }); - - expect(pullFilesFetches).toBeGreaterThan(0); - expect((await listPullRequestFiles(env, "JSONbored/gittensory", 49)).map((file) => file.path)).toEqual(["migrations/0099_security.sql"]); - }); - - it("pre-merge checks (#review-pre-merge-checks): an enforced check that fails blocks the auto-merge", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "observe", request_changes: "observe" }, // evaluate + post the gate, take no merge/close action - agentDryRun: false, // so the gate check-run is actually POSTed (dry-run suppresses the write) and capturable - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - // The maintainer requires the "approved" label before merge — DETERMINISTIC, enforced. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { pre_merge_checks: [{ name: "Approved label required", require_label: "approved", enforce: true }] } }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 49, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); - - let gateConclusion: string | undefined; - let gateText = ""; - const captureGate = (body: { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }) => { - if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { - gateConclusion = body.conclusion; - gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; - } - }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) { - if (init?.body) captureGate(JSON.parse(init.body.toString())); - return Response.json({ id: 901 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "pre-merge-check-block", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 49, - title: "feat: add a feature", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate124" }, - labels: [], // missing the required "approved" label → the enforced check FAILS - body: "Closes #1", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - // The enforced pre-merge check failed → the gate check-run is a FAILURE that names the specific check. - expect(gateConclusion).toBe("failure"); - expect(gateText).toContain("Pre-merge check not satisfied: Approved label required"); - }); - - it("CLA gate (#2564): claMode: block + a missing consent phrase blocks the auto-merge (acceptance criterion)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "observe", request_changes: "observe" }, - agentDryRun: false, - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { consentPhrase: "I have read and agree to the CLA" } } }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 49, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); - - let gateConclusion: string | undefined; - let gateText = ""; - const captureGate = (body: { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }) => { - if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { - gateConclusion = body.conclusion; - gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; - } - }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) { - if (init?.body) captureGate(JSON.parse(init.body.toString())); - return Response.json({ id: 901 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "cla-gate-block", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 49, - title: "feat: add a feature", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate126" }, - labels: [], - body: "Closes #1", // missing the required CLA consent phrase → the gate FAILS - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - // The CLA consent phrase is missing → the gate check-run is a FAILURE naming the CLA finding. - expect(gateConclusion).toBe("failure"); - expect(gateText).toContain("CLA consent not confirmed"); - }); - - it("CLA gate (#2564): claMode: block + the consent phrase present in the PR body passes the gate", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "observe", request_changes: "observe" }, - agentDryRun: false, - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { consentPhrase: "I have read and agree to the CLA" } } }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 50, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); - - let gateConclusion: string | undefined; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) { - if (init?.body) { - const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string }; - if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) gateConclusion = body.conclusion; - } - return Response.json({ id: 902 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "cla-gate-pass", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 50, - title: "feat: add a feature", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate127" }, - labels: [], - body: "Closes #1\n\nI have read and agree to the CLA.", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - expect(gateConclusion).not.toBe("failure"); - }); - - it("CLA gate (#2564) is OFF by default: no manifest opt-in ⇒ a PR with no CLA consent still passes (zero behavior change)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "observe", request_changes: "observe" }, - agentDryRun: false, - // No gate.claMode manifest override — claGateMode stays undefined (the safe default). - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 51, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); - - let gateConclusion: string | undefined; - let gateText = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) { - if (init?.body) { - const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }; - if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { - gateConclusion = body.conclusion; - gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; - } - } - return Response.json({ id: 903 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "cla-gate-off-default", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 51, - title: "feat: add a feature", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate128" }, - labels: [], - body: "Closes #1", // no CLA consent anywhere — must not matter when claMode is off - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - expect(gateConclusion).not.toBe("failure"); - expect(gateText).not.toContain("CLA consent not confirmed"); - }); - - it("CLA gate (#2564): check-run-conclusion detection — a passing named CLA-bot check-run satisfies consent", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "observe", request_changes: "observe" }, - agentDryRun: false, - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - // Check-run-only config: no consentPhrase, so ONLY the named check-run's conclusion is consulted. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { checkRunName: "CLA Assistant Lite", checkRunAppSlug: "cla-assistant" } } }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 52, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); - - let gateConclusion: string | undefined; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/commits/gate129/check-runs")) { - return Response.json({ total_count: 1, check_runs: [{ id: 1, name: "CLA Assistant Lite", status: "completed", conclusion: "success", app: { slug: "cla-assistant" } }] }); - } - if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) { - if (init?.body) { - const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string }; - if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) gateConclusion = body.conclusion; - } - return Response.json({ id: 904 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "cla-gate-checkrun-pass", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 52, - title: "feat: add a feature", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate129" }, - labels: [], - body: "Closes #1", // no phrase — consent comes entirely from the check-run - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - expect(gateConclusion).not.toBe("failure"); - }); - - it("CLA gate (#2564): check-run-conclusion detection — a failing named CLA-bot check-run blocks the auto-merge", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "observe", request_changes: "observe" }, - agentDryRun: false, - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { checkRunName: "CLA Assistant Lite", checkRunAppSlug: "cla-assistant" } } }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 53, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); - - let gateConclusion: string | undefined; - let gateText = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/commits/gate130/check-runs")) { - return Response.json({ total_count: 1, check_runs: [{ id: 2, name: "CLA Assistant Lite", status: "completed", conclusion: "failure", app: { slug: "cla-assistant" } }] }); - } - if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) { - if (init?.body) { - const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }; - if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { - gateConclusion = body.conclusion; - gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; - } - } - return Response.json({ id: 905 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "cla-gate-checkrun-fail", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 53, - title: "feat: add a feature", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate130" }, - labels: [], - body: "Closes #1", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - expect(gateConclusion).toBe("failure"); - expect(gateText).toContain("CLA consent not confirmed"); - }); - - it("REGRESSION (gate finding): CLA gate (#2564) — a check-run-only config missing checkRunAppSlug BLOCKS the auto-merge instead of silently holding forever", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "observe", request_changes: "observe" }, - agentDryRun: false, - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - // Misconfigured: checkRunName set, checkRunAppSlug forgotten -- no run can ever be trusted, so the gate - // must BLOCK (not hold), even though a same-name check-run with a passing conclusion exists on the commit. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { claMode: "block", cla: { checkRunName: "CLA Assistant Lite" } } }); - await upsertPullRequestFile(env, { repoFullName: "JSONbored/gittensory", pullNumber: 54, path: "src/feature.ts", status: "modified", additions: 5, deletions: 0, changes: 5, payload: {} }); - - let gateConclusion: string | undefined; - let gateText = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - // Even though a same-name check-run with a passing conclusion exists on the commit, the missing - // checkRunAppSlug means fetchNamedCheckRunConclusion never gets far enough to see it (returns null - // before any check-runs fetch) -- the gate must still see it as blocking, not "not evaluated". - if (url.includes("/commits/gate131/check-runs")) { - return Response.json({ total_count: 1, check_runs: [{ id: 3, name: "CLA Assistant Lite", status: "completed", conclusion: "success", app: { slug: "cla-assistant" } }] }); - } - if (url.includes("/commits/") && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) { - if (init?.body) { - const body = JSON.parse(init.body.toString()) as { name?: string; conclusion?: string; output?: { title?: string; summary?: string } }; - if ((body.name ?? "").includes("Gittensory Orb Review Agent") && body.conclusion) { - gateConclusion = body.conclusion; - gateText = `${body.output?.title ?? ""} ${body.output?.summary ?? ""}`; - } - } - return Response.json({ id: 906 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "cla-gate-checkrun-missing-slug", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 54, - title: "feat: add a feature", - state: "open", - user: { login: "contributor" }, - head: { sha: "gate131" }, - labels: [], - body: "Closes #1", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - expect(gateConclusion).toBe("failure"); - expect(gateText).toContain("CLA consent not confirmed"); - }); - - async function setupPlannerRepo(env: Env): Promise { - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, repository_selection: "selected", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["issues", "issue_comment"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - } - - function plannerWebhook(commentBody: string, sender: string, issueOverride?: Record): Parameters[1] { - return { - type: "github-webhook", - deliveryId: `plan-${sender}-${commentBody.length}-${issueOverride ? "pr" : "issue"}`, - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: issueOverride ?? { number: 77, title: "Add a retry to the fetch helper", state: "open", user: { login: "reporter" }, body: "The fetch helper should retry on 5xx." }, - comment: { body: commentBody, user: { login: sender, type: "User" } }, - sender: { login: sender, type: "User" }, - }, - } as unknown as Parameters[1]; - } - - it("planner (#issue-coding-plan): a maintainer @gittensory plan on an issue posts an AI plan (flag ON)", async () => { - const run = vi.fn(async () => ({ response: "## Summary\nAdd retry-on-5xx to the fetch helper.\n\n## Steps\n1. Wrap the fetch in a retry loop." })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - let postedBody: string | undefined; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer - if (url.includes("/issues/77/comments")) { - postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); - expect(run).toHaveBeenCalledTimes(1); - expect(postedBody).toContain("Gittensory implementation plan"); - expect(postedBody).toContain("Add retry-on-5xx"); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.issue_plan_generated").first<{ n: number }>(); - expect(audit?.n).toBe(1); - const usage = await env.DB.prepare("select feature, actor, status, estimated_neurons, metadata_json from ai_usage_events where feature = ?").bind("issue_plan").first<{ feature: string; actor: string; status: string; estimated_neurons: number; metadata_json: string }>(); - expect(usage?.status).toBe("ok"); - expect(usage?.actor).toBe("maintainer1"); - expect(usage?.estimated_neurons).toBeGreaterThan(0); - expect(JSON.parse(usage?.metadata_json ?? "{}")).toMatchObject({ repoFullName: "JSONbored/gittensory", issueNumber: 77 }); - }); - - it("planner: enforces the shared AI budget before calling Workers AI", async () => { - const run = vi.fn(async () => ({ response: "should not run" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI_DAILY_NEURON_BUDGET: "0", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); - expect(run).not.toHaveBeenCalled(); - const usage = await env.DB.prepare("select status from ai_usage_events where feature = ?").bind("issue_plan").first<{ status: string }>(); - expect(usage?.status).toBe("quota_exceeded"); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("no_plan_generated"); - }); - - it("planner: respects agentPaused — never spends Workers AI on a paused repo (#2257)", async () => { - const run = vi.fn(async () => ({ response: "should not run" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); - expect(run).not.toHaveBeenCalled(); // no speculative AI spend on a paused repo - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("agent_paused"); - }); - - it("planner: respects a global freeze — never spends Workers AI while the DB kill-switch is engaged (#2257)", async () => { - const run = vi.fn(async () => ({ response: "should not run" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AGENT_ACTIONS_PAUSED: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); - expect(run).not.toHaveBeenCalled(); - }); - - it("planner: respects agentDryRun — never spends Workers AI on a dry-run repo (#2257)", async () => { - const run = vi.fn(async () => ({ response: "should not run" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); - expect(run).not.toHaveBeenCalled(); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("dry_run"); - }); - - it("planner: enforces a per-actor per-repo cooldown before spending AI", async () => { - const run = vi.fn(async () => ({ response: "## Summary\nPlan." })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/77/comments")) return Response.json({ id: init?.body ? 5 : 6 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); - await processJob(env, plannerWebhook("@gittensory plan again", "maintainer1")); - expect(run).toHaveBeenCalledTimes(1); - const cooldown = await env.DB.prepare("select detail from audit_events where event_type = ? and detail = ?").bind("github_app.issue_plan_skipped", "cooldown_active").first<{ detail: string }>(); - expect(cooldown?.detail).toBe("cooldown_active"); - }); - - it("planner: flag OFF is byte-identical — @gittensory plan posts no plan and the AI is never called", async () => { - const run = vi.fn(async () => ({ response: "should not run" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "false", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - let postedPlan = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/77/comments")) { - if (init?.body && JSON.parse(init.body.toString()).body?.includes("implementation plan")) postedPlan = true; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); - expect(run).not.toHaveBeenCalled(); - expect(postedPlan).toBe(false); - }); - - it("planner: a NON-maintainer is denied — no plan is generated or posted (flag ON)", async () => { - const run = vi.fn(async () => ({ response: "should not run" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not a maintainer - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "outsider")); - expect(run).not.toHaveBeenCalled(); - const denied = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); - // Authorization now flows through the per-repo commandAuthorization policy (#21), so the skip reason is the - // policy's verdict (not the old bespoke "actor_not_maintainer"). - expect(denied?.detail).toBe("not_maintainer_or_pr_author"); - }); - - it("planner (#21): honors a per-repo commandAuthorization override that restricts `plan` to maintainers", async () => { - const run = vi.fn(async () => ({ response: "should not run" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - // Override: `plan` is maintainer-ONLY (drop the default collaborator role). - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { plan: ["maintainer"] } } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "write" }); // collaborator, not maintainer - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "collab1")); - expect(run).not.toHaveBeenCalled(); - const denied = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); - expect(denied?.detail).toBe("not_maintainer_or_pr_author"); - }); - - - it("planner: a flag-ON non-plan comment is not intercepted (the handler declines)", async () => { - const run = vi.fn(async () => ({ response: "nope" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); - await processJob(env, plannerWebhook("just a normal comment with no command", "maintainer1")); - expect(run).not.toHaveBeenCalled(); // not a plan command → maybeProcessPlanCommand returns false, no AI spend - }); - - it("planner (#22): @gittensory plan on a PR is NOT consumed — it falls through (no plan, no skip audit)", async () => { - const run = vi.fn(async () => ({ response: "nope" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - vi.stubGlobal("fetch", async () => Response.json({})); - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1", { number: 77, title: "PR not issue", state: "open", user: { login: "x" }, body: "b", pull_request: { url: "https://api.github.com/x" } })); - expect(run).not.toHaveBeenCalled(); - // Planning is issue-only; a PR-thread `plan` falls through to the mention/help path (flag-ON now matches - // flag-OFF) instead of being swallowed as a plan skip. - const planAudits = await env.DB.prepare("select count(*) as n from audit_events where event_type in (?, ?)").bind("github_app.issue_plan_skipped", "github_app.issue_plan_generated").first<{ n: number }>(); - expect(planAudits?.n).toBe(0); - }); - - it("planner: a bot-authored @gittensory plan on an issue is recorded as a classifier skip", async () => { - const run = vi.fn(async () => ({ response: "nope" })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - vi.stubGlobal("fetch", async () => Response.json({})); - await processJob(env, { - type: "github-webhook", - deliveryId: "plan-bot", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Issue", state: "open", user: { login: "reporter" }, body: "b" }, - comment: { body: "@gittensory plan", user: { login: "some-bot[bot]", type: "Bot" } }, - sender: { login: "some-bot[bot]", type: "Bot" }, - }, - } as unknown as Parameters[1]); - expect(run).not.toHaveBeenCalled(); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("unsupported_comment_action_or_bot"); - }); - - it("planner: a maintainer request that yields no plan is recorded as a skip (fail-safe)", async () => { - const run = vi.fn(async () => ({ response: " " })); // model returns nothing usable - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_PLANNER: "true", AI: { run } as unknown as Ai }); - await setupPlannerRepo(env); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "write" }); // maintainer - if (url.includes("/issues/77/comments")) { - posted = true; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory plan", "maintainer1")); - expect(posted).toBe(false); // no plan → nothing posted - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.issue_plan_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("no_plan_generated"); - }); - - it("configuration (#2168): a maintainer @gittensory configuration posts the effective resolved config", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupPlannerRepo(env); - let postedBody: string | undefined; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer - if (url.includes("/issues/77/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/77/comments") && method === "POST") { - postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory configuration", "maintainer1")); - expect(postedBody).toContain("Effective review configuration"); - expect(postedBody).toContain("Agent execution mode: **live**"); - expect(postedBody).toContain("Autonomy by action class:"); - // public-safe: never leaks a reward/trust/wallet field - expect(postedBody?.toLowerCase()).not.toMatch(/reward|wallet|hotkey|coldkey|trustscore/); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.configuration_posted").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - }); - - it.each([ - ["env pause", async (env: Env) => { (env as Env & { AGENT_ACTIONS_PAUSED: string }).AGENT_ACTIONS_PAUSED = "true"; }, "paused"], - ["repo pause", async (env: Env) => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); }, "paused"], - ["repo dry-run", async (env: Env) => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true }); }, "dry_run"], - ["DB global freeze", async (env: Env) => { await setGlobalAgentFrozen(env, true); }, "paused"], - ] as const)("configuration respects %s — never posts the effective-config comment live", async (_label, applyPause, expectedMode) => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupPlannerRepo(env); - await applyPause(env); - const calls = { commentPosts: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/77/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/77/comments") && method === "POST") { - calls.commentPosts += 1; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, plannerWebhook("@gittensory configuration", "maintainer1")); - - expect(calls.commentPosts).toBe(0); - const audit = await env.DB.prepare("select json_extract(metadata_json, '$.mode') as mode from audit_events where event_type = ?").bind("github_app.configuration_posted").first<{ mode: string }>(); - expect(audit?.mode).toBe(expectedMode); - }); - - it("configuration: a non-maintainer is denied — nothing is posted and a skip is recorded", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupPlannerRepo(env); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not a maintainer - if (url.includes("/issues/77/comments")) { - posted = true; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory configuration", "outsider")); - expect(posted).toBe(false); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.configuration_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("not_maintainer_or_pr_author"); - }); - - it("configuration: a non-configuration comment is not intercepted (the handler declines, no config audit)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupPlannerRepo(env); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); - await processJob(env, plannerWebhook("just a normal comment, no mention", "maintainer1")); - const posted = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.configuration_posted").first(); - const skipped = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.configuration_skipped").first(); - expect(posted).toBeFalsy(); - expect(skipped).toBeFalsy(); - }); - - it("configuration: a bot-authored command is recorded as a classifier skip, never posted", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupPlannerRepo(env); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "config-bot", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "t", state: "open", user: { login: "reporter" }, body: "b" }, - comment: { body: "@gittensory configuration", user: { login: "some-bot[bot]", type: "Bot" } }, - sender: { login: "some-bot[bot]", type: "Bot" }, - }, - } as unknown as Parameters[1]); - expect(posted).toBe(false); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.configuration_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("unsupported_comment_action_or_bot"); - }); - - const pauseIssue = { number: 77, title: "Add a retry to the fetch helper", state: "open", user: { login: "reporter" }, body: "b", pull_request: { url: "https://api.github.com/repos/JSONbored/gittensory/pulls/77" } }; - async function seedPausePr(env: Env): Promise { - await setupPlannerRepo(env); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 77, title: "Add a retry to the fetch helper", state: "open", user: { login: "reporter" }, head: { sha: "h1" }, labels: [], body: "b" }); - } - // Mirrors hasAutoreviewPausedMarker's own MOST-RECENT-of-{paused,resumed} query (#2165) via a raw read, - // rather than exporting that internal helper just for tests -- same pattern the pre-existing pause tests - // already use (raw audit_events queries) instead of importing processors.ts internals. - async function isCurrentlyPaused(env: Env, repoFullName: string, prNumber: number): Promise { - const row = await env.DB.prepare( - "select event_type from audit_events where event_type in (?, ?) and target_key = ? and outcome = ? order by created_at desc, rowid desc limit 1", - ) - .bind("github_app.autoreview_paused", "github_app.autoreview_resumed", `${repoFullName}#${prNumber}`, "completed") - .first<{ event_type: string }>(); - return row?.event_type === "github_app.autoreview_paused"; - } - - it("pause (#2164): a maintainer @gittensory pause records the autoreview-paused marker and posts a public-safe confirmation", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - let postedBody: string | undefined; - const urls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - urls.push(url); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer - if (url.includes("/issues/77/comments")) { - postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory pause flaky CI, will re-enable after the fix", "maintainer1", pauseIssue)); - expect(postedBody).toContain("Auto-review paused by @maintainer1"); - expect(postedBody).toContain("Gate enforcement and the one-shot disposition are unchanged"); - expect(postedBody).toContain("flaky CI, will re-enable after the fix"); - // AUTO-REVIEW SCOPE ONLY (#2164): no Gate check-run is written and no gate-disposition audit is recorded, so the - // one-shot gate/advisory is provably untouched. - expect(urls.some((u) => u.includes("/check-runs"))).toBe(false); - const gateAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.gate_%'").first<{ n: number }>(); - expect(gateAudit?.n).toBe(0); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.autoreview_paused").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toBe("flaky CI, will re-enable after the fix"); - const usage = await env.DB.prepare("select outcome from product_usage_events where event_name = ?").bind("autoreview_paused").first<{ outcome: string }>(); - expect(usage?.outcome).toBe("completed"); - }); - - it("pause: an authorized pause with no trailing reason records the marker with a 'No reason provided.' detail", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - let postedBody: string | undefined; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/77/comments")) { - postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); - expect(postedBody).toContain("No reason provided."); - const audit = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_paused").first<{ detail: string }>(); - expect(audit?.detail).toBe("No reason provided."); - }); - - it("pause: a non-maintainer is denied — nothing is posted and a denied marker is recorded (never a pause)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not a maintainer - if (url.includes("/issues/77/comments")) { - posted = true; - return Response.json({ id: 5 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory pause let me in", "outsider", pauseIssue)); - expect(posted).toBe(false); - const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.autoreview_paused_denied").first<{ outcome: string }>(); - expect(denied?.outcome).toBe("denied"); - const paused = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.autoreview_paused").first(); - expect(paused).toBeFalsy(); - }); - - it("pause: a pause on a PR with no cached record is recorded as a cached_pr_missing skip, never posted", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupPlannerRepo(env); // repo + installation, but deliberately NO cached PR record - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); - expect(posted).toBe(false); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_paused_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("cached_pr_missing"); - }); - - it("pause: a bot-authored @gittensory pause is recorded as a classifier skip, never posted", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "pause-bot", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: pauseIssue, - comment: { body: "@gittensory pause", user: { login: "some-bot[bot]", type: "Bot" } }, - sender: { login: "some-bot[bot]", type: "Bot" }, - }, - } as unknown as Parameters[1]); - expect(posted).toBe(false); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_paused_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("bot_author"); - }); - - it("pause: a non-pause comment is not intercepted (the handler declines, no autoreview audit)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); - await processJob(env, plannerWebhook("just a normal comment, no mention", "maintainer1", pauseIssue)); - const paused = await env.DB.prepare("select 1 from audit_events where event_type like 'github_app.autoreview_paused%'").first(); - expect(paused).toBeFalsy(); - }); - - const reviewIssue = { number: 78, title: "Draft feature for review command", state: "open", user: { login: "reporter" }, body: "b", pull_request: { url: "https://api.github.com/repos/JSONbored/gittensory/pulls/78" } }; - async function seedReviewPr(env: Env, options: { draft?: boolean } = {}): Promise { - await setupPlannerRepo(env); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { skip_drafts: true } } }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 78, title: "Draft feature for review command", state: "open", draft: options.draft ?? true, user: { login: "reporter" }, head: { sha: "r78" }, labels: [], body: "b" }); - await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 78, status: "complete", reviewsSyncedAt: new Date().toISOString() }); - } - function reviewCommandFetchStub(): (input: RequestInfo | URL, init?: RequestInit) => Promise { - const seen: string[] = []; - return async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - seen.push(url); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer - if (url.includes("/pulls/78/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/78")) return Response.json({ number: 78, title: "Draft feature for review command", state: "open", draft: true, user: { login: "reporter" }, head: { sha: "r78" }, labels: [], body: "b", mergeable_state: "clean" }); - if (url.includes("/commits/r78/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/r78/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - if (url.includes("/issues/78/comments") && method === "POST") return Response.json({ id: 78 }, { status: 201 }); - if (url.includes("/issues/78/comments")) return Response.json([]); - if (url.includes("/check-runs") && (method === "POST" || method === "PATCH")) return Response.json({ id: 981 }, { status: method === "POST" ? 201 : 200 }); - return Response.json({}); - }; - } - - it("review (#2163): an authorized @gittensory review posts a confirmation, dispatches a REAL re-review (proven by a live PR resync fetch inside reReviewStoredPullRequest, not just the command's own comment post), and records review_command_completed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewPr(env); - let postedCommentBody: string | undefined; - let liveResyncFetched = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/issues/78/comments") && method === "POST") { - postedCommentBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; - return Response.json({ id: 78 }, { status: 201 }); - } - // reReviewStoredPullRequest's own live-head resync (#sweep-resync) GETs the PR fresh before reviewing -- - // this only happens INSIDE that function, never in the command handler's own classify/authorize/confirm - // steps, so seeing it proves the dispatch call genuinely reached the real re-review path. - if (url.endsWith("/pulls/78") && method === "GET") liveResyncFetched = true; - return reviewCommandFetchStub()(input, init); - }); - await processJob(env, plannerWebhook("@gittensory review", "maintainer1", reviewIssue)); - expect(postedCommentBody).toContain("Re-review triggered by @maintainer1"); - expect(liveResyncFetched).toBe(true); // proves the real reReviewStoredPullRequest path ran, unlike pause - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_command_completed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - const usage = await env.DB.prepare("select outcome from product_usage_events where event_name = ?").bind("review_command_completed").first<{ outcome: string }>(); - expect(usage?.outcome).toBe("completed"); - // The command itself never writes repository_settings -- it only triggers a fresh eval through the same - // path a scheduled sweep would take (#2163's hard constraint: never reimplements/flips the disposition). - const settingsRow = await env.DB.prepare("select 1 from repository_settings where repo_full_name = ?").bind("JSONbored/gittensory").first(); - expect(settingsRow).toBeFalsy(); - }); - - it("review: the 're-review' alias resolves to the same handler", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewPr(env); - let postedCommentBody: string | undefined; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/issues/78/comments") && method === "POST") { - postedCommentBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; - return Response.json({ id: 78 }, { status: 201 }); - } - return reviewCommandFetchStub()(input, init); - }); - await processJob(env, plannerWebhook("@gittensory re-review", "maintainer1", reviewIssue)); - expect(postedCommentBody).toContain("Re-review triggered by @maintainer1"); - }); - - it("review: a non-maintainer/collaborator/confirmed-miner is denied — nothing posted, no re-review dispatched", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewPr(env); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not authorized - if (url.includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory review", "outsider", reviewIssue)); - expect(posted).toBe(false); - const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_command_denied").first<{ outcome: string }>(); - expect(denied?.outcome).toBe("denied"); - const completed = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.review_command_completed").first(); - expect(completed).toBeFalsy(); - }); - - // REGRESSION: DEFAULT_COMMAND_AUTHORIZATION_POLICY deliberately widens "review" to confirmed_miner (a - // confirmed miner may re-trigger review on their own PR, the same self-rerun precedent as review-now). That - // requires authorizePrActionActor's needsMinerDetection: true -- an earlier version of this handler omitted - // it, so a confirmed miner's OWN PR author (not a maintainer/collaborator) was wrongly denied every time, - // since there was no other role they could match instead. - it("review: a confirmed Gittensor miner is authorized to re-review their OWN PR (not a maintainer/collaborator)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewPr(env); - await upsertOfficialMinerDetection(env, "reporter", { status: "confirmed", snapshot: queueMinerSnapshot("reporter") }, 60_000); - // A confirmed miner is ALSO a confirmedContributor for the dispatched reReviewStoredPullRequest's own - // public-surface eligibility, so this pass can post a SECOND, unrelated deterministic panel comment - // alongside the review command's own confirmation -- collect every posted body rather than assuming - // the command's confirmation is the only (or the last) one. - const postedBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/collaborators/") && url.includes("/permission")) return new Response("not found", { status: 404 }); // no repo permission at all - if (url.includes("/issues/78/comments") && method === "POST") { - postedBodies.push(init?.body ? JSON.parse(init.body.toString()).body : ""); - return Response.json({ id: 78 }, { status: 201 }); - } - return reviewCommandFetchStub()(input, init); - }); - - await processJob(env, plannerWebhook("@gittensory review", "reporter", reviewIssue)); - - expect(postedBodies.some((body) => body.includes("Re-review triggered by @reporter"))).toBe(true); - const completed = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_command_completed").first<{ outcome: string }>(); - expect(completed?.outcome).toBe("completed"); - const denied = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.review_command_denied").first(); - expect(denied).toBeFalsy(); - const forceBypass = await env.DB.prepare("select 1 from audit_events where event_type = ? and target_key = ?").bind("github_app.ai_review_force_bypass", "JSONbored/gittensory#78").first(); - expect(forceBypass).toBeFalsy(); - }); - - it("review: respects agentPaused and agentDryRun without dispatching re-review", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewPr(env); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); - vi.stubGlobal("fetch", reviewCommandFetchStub()); - - await processJob(env, plannerWebhook("@gittensory review", "maintainer1", reviewIssue)); - let skipped = await env.DB.prepare("select detail from audit_events where event_type = ? order by rowid desc limit 1").bind("github_app.review_command_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("agent_paused"); - let completed = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.review_command_completed").first(); - expect(completed).toBeFalsy(); - - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: false, agentDryRun: true }); - await processJob(env, plannerWebhook("@gittensory review", "maintainer1", reviewIssue)); - skipped = await env.DB.prepare("select detail from audit_events where event_type = ? order by rowid desc limit 1").bind("github_app.review_command_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("dry_run"); - completed = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.review_command_completed").first(); - expect(completed).toBeFalsy(); - }); - - it("review: a review command on a PR with no cached record is recorded as a cached_pr_missing skip, never posted", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupPlannerRepo(env); // repo + installation, but deliberately NO cached PR record - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory review", "maintainer1", reviewIssue)); - expect(posted).toBe(false); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.review_command_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("cached_pr_missing"); - }); - - it("review: a bot-authored @gittensory review is recorded as a classifier skip, never posted", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewPr(env); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "review-bot", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: reviewIssue, - comment: { body: "@gittensory review", user: { login: "some-bot[bot]", type: "Bot" } }, - sender: { login: "some-bot[bot]", type: "Bot" }, - }, - } as unknown as Parameters[1]); - expect(posted).toBe(false); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.review_command_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("bot_author"); - }); - - it("resume (#2165): an authorized @gittensory resume clears an earlier pause and posts a public-safe confirmation", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/77/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 5 }, { status: 201 }); - if (url.includes("/issues/77/comments")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - // Pause first, matching real usage: a resume without a prior pause is still valid (idempotent), but this - // proves the SUPERSEDE behavior, not just that resume can run standalone. - await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); - expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(true); - - let postedBody: string | undefined; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/77/comments")) { - postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; - return Response.json({ id: 6 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory resume", "maintainer1", pauseIssue)); - expect(postedBody).toContain("Auto-review resumed by @maintainer1"); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.autoreview_resumed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - // The core bug fix (#2165): hasAutoreviewPausedMarker now reads the MOST RECENT of {paused, resumed}, so - // resume actually supersedes the earlier pause instead of silently no-opping forever. - expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(false); - }); - - it("resume: a LATER pause after a resume still re-pauses correctly (ordering, not just existence)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - const adminFetch = (): ((input: RequestInfo | URL, init?: RequestInit) => Promise) => async (input, init) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/77/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 5 }, { status: 201 }); - if (url.includes("/issues/77/comments")) return Response.json([]); - return new Response("not found", { status: 404 }); - }; - vi.stubGlobal("fetch", adminFetch()); - await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); - expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(true); - vi.stubGlobal("fetch", adminFetch()); - await processJob(env, plannerWebhook("@gittensory resume", "maintainer1", pauseIssue)); - expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(false); - vi.stubGlobal("fetch", adminFetch()); - await processJob(env, plannerWebhook("@gittensory pause again", "maintainer1", pauseIssue)); - expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(true); - }); - - it("resume: a non-maintainer/collaborator is denied — nothing posted and the pause marker is untouched", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/77/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 5 }, { status: 201 }); - if (url.includes("/issues/77/comments")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory pause", "maintainer1", pauseIssue)); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "read" }); // not authorized - if (url.includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory resume", "outsider", pauseIssue)); - expect(posted).toBe(false); - const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.autoreview_resumed_denied").first<{ outcome: string }>(); - expect(denied?.outcome).toBe("denied"); - expect(await isCurrentlyPaused(env, "JSONbored/gittensory", 77)).toBe(true); // still paused - }); - - it("resume: a resume on a PR with no cached record is recorded as a cached_pr_missing skip, never posted", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupPlannerRepo(env); // repo + installation, but deliberately NO cached PR record - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, plannerWebhook("@gittensory resume", "maintainer1", pauseIssue)); - expect(posted).toBe(false); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_resumed_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("cached_pr_missing"); - }); - - it("resume: a bot-authored @gittensory resume is recorded as a classifier skip, never posted", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedPausePr(env); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "resume-bot", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: pauseIssue, - comment: { body: "@gittensory resume", user: { login: "some-bot[bot]", type: "Bot" } }, - sender: { login: "some-bot[bot]", type: "Bot" }, - }, - } as unknown as Parameters[1]); - expect(posted).toBe(false); - const skip = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.autoreview_resumed_skipped").first<{ detail: string }>(); - expect(skip?.detail).toBe("bot_author"); - }); - - it("REGRESSION (#audit-draft-maintenance): a clean DRAFT PR is never auto-merged/approved/closed (drafts are WIP)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - action: "created", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - target_type: "User", - repository_selection: "all", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // Clean, mergeable, approved, green CI + merge:auto + close:auto + approve:auto — a NON-draft here would be - // auto-acted. The ONLY thing that must stop it is the draft guard in maybeRunAgentMaintenance. - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "auto", approve: "auto", close: "auto" }, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/draft1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/draft1/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) return Response.json({ id: 901 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "draft-no-maintenance", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 49, - title: "Work in progress", - state: "open", - draft: true, - user: { login: "contributor" }, - head: { sha: "draft1" }, - labels: [], - body: "Closes #1", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - - // No terminal maintenance action of ANY class fires on a draft. - const acted = await env.DB.prepare("select count(*) as n from audit_events where event_type in ('agent.action.merge','agent.action.approve','agent.action.close')").first<{ n: number }>(); - expect(acted?.n).toBe(0); - }); - - it("blacklist (#1425): a banned author's PR is labeled + closed deterministically with NO AI call and no merit merge", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "n/a", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - // The banned login is per-repo DB config; the label is the configurable `.gittensory.yml` value below — - // nothing is hard-coded. - contributorBlacklist: [{ login: "baduser", reason: "plagiarism" }], - }); - // The label is configurable via `.gittensory.yml` (default "slop"); set a custom one to prove it's not hardcoded. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { blacklistLabel: "spam" } }, "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(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "baduser" }, head: { sha: "bl55" }, mergeable_state: "clean" }); - if (url.includes("/commits/bl55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/bl55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/55/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "blacklist-close", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Banned author PR", state: "open", user: { login: "baduser" }, head: { sha: "bl55" }, labels: [], body: "Closes #1", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - // Deterministic gate: closed + labeled (with the configured label), and the AI was NEVER called. - expect(aiCalls).toBe(0); - expect(seen.closed).toBe(true); - expect(seen.labels).toContain("spam"); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - // No merit merge despite a clean+green+approved PR (the blacklist short-circuits ahead of merit). - const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); - expect(mergeAudit?.n).toBe(0); - // The close comment is public-safe and explains the block. - expect(seen.comments.some((c) => c.includes("blocked from contributing"))).toBe(true); - }); - - it("screenshot-table gate (#2006): an in-scope contributor PR missing a before/after table is closed deterministically with NO AI call and no merit merge", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "n/a", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - }); - // Scoped to the `visual` label only, config-as-code, nothing hardcoded — mirrors the blacklistLabel test above. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { screenshotTableGate: { enabled: true, whenLabels: ["visual"] } } }, "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(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/56/files")) return Response.json([{ filename: "apps/ui/src/App.tsx", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/56/reviews")) return Response.json([]); - if (url.includes("/pulls/56/commits")) return Response.json([]); - if (url.endsWith("/pulls/56") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 56, state: "closed" }); } - if (url.endsWith("/pulls/56")) return Response.json({ number: 56, state: "open", user: { login: "visual-contributor" }, head: { sha: "vis56" }, mergeable_state: "clean" }); - if (url.includes("/commits/vis56/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/vis56/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/56/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/56/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/56/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } - if (url.includes("/issues/56/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "screenshot-table-close", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 56, title: "New button color", state: "open", user: { login: "visual-contributor" }, head: { sha: "vis56" }, labels: [{ name: "visual" }], body: "Changed the button color. Closes #1", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - // Deterministic gate: closed, and the AI was NEVER called for the disposition. - expect(aiCalls).toBe(0); - expect(seen.closed).toBe(true); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - // No merit merge despite a clean+green+approved PR (the screenshot-table gate short-circuits ahead of merit). - const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); - expect(mergeAudit?.n).toBe(0); - // The close comment explains the missing table. - expect(seen.comments.some((c) => c.includes("before/after screenshot table"))).toBe(true); - }); - - it("screenshot-table gate (#2006): an in-scope PR WITH a valid before/after table is NOT closed by the gate (no false-positive)", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto", merge: "auto" }, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { screenshotTableGate: { enabled: true, whenLabels: ["visual"] } } }, "repo_file"); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/57/files")) return Response.json([{ filename: "apps/ui/src/App.tsx", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/57/reviews")) return Response.json([]); - if (url.includes("/pulls/57/commits")) return Response.json([]); - if (url.endsWith("/pulls/57") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 57, state: "closed" }); } - if (url.endsWith("/pulls/57")) return Response.json({ number: 57, state: "open", user: { login: "visual-contributor" }, head: { sha: "vis57" }, mergeable_state: "clean" }); - if (url.includes("/commits/vis57/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/vis57/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - if (url.includes("/issues/57/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/57/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "screenshot-table-pass", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 57, - title: "New button color", - state: "open", - user: { login: "visual-contributor" }, - head: { sha: "vis57" }, - labels: [{ name: "visual" }], - body: "Changed the button color.\n\n| Before | After |\n| --- | --- |\n| ![before](https://x/before.png) | ![after](https://x/after.png) |\n\nCloses #1", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - - // The valid before/after table means the deterministic gate never matches — no close of any kind fires. - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBe(0); - }); - - // #4110: same in-scope, NO-body-table fixture as the "closed deterministically" test above (a hand-authored - // table would normally be the ONLY way to avoid the close) -- the ONLY difference is that this PR ALSO - // touches a web-visible route file with a real, resolvable preview deploy. Proves the marker - // (markPullRequestVisualCaptureSatisfied) is READ BACK correctly (evaluateScreenshotTableGate's - // botCaptureSatisfied) without a hand-authored table. - // - // #4136: isPersistedShotUrl now requires a real `key=` R2 URL, which only a genuine Browser Rendering pass - // can produce (env.BROWSER is unavailable in this unit-test environment, so buildCapture always falls back - // to a placeholder here -- covered separately by test/unit/visual-shot.test.ts's own captureShot mocking). - // Rather than mock a full headless-browser launch just to exercise this gate-read-back assertion, this - // seeds the marker the SAME way production does: markPullRequestVisualCaptureSatisfied is called by an - // EARLIER pass (a `synchronize` capture) at this exact head SHA, before the webhook under test runs. This - // is not a weaker test of the real behavior -- capture and gate evaluation routinely happen on different - // webhook deliveries in production (buildCapture runs on `synchronize`; the maintenance pass that reads the - // marker back can fire later, e.g. a re-gate sweep) -- and it still fully proves the read-back half of the - // #4110 gate: upsertPullRequestFromGitHub's own onConflict clause never touches visualCaptureSatisfiedSha - // (see its own comment), so the marker survives this webhook's PR upsert untouched, exactly as it would - // survive any later webhook in production. - it("screenshot-table gate (#4110): a persisted bot capture from an earlier pass satisfies the gate, no body table needed", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", - GITTENSORY_REVIEW_SCREENSHOTS: "true", - }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto", label: "auto" }, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { screenshotTableGate: { enabled: true, whenLabels: ["visual"] } } }, "repo_file"); - // Simulates an earlier `synchronize` pass whose real (Browser Rendering) capture already succeeded at - // this head SHA and persisted the marker -- see the test doc comment above. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 58, - title: "Update the app index route", - state: "open", - user: { login: "visual-contributor" }, - head: { sha: "vis58" }, - labels: [{ name: "visual" }], - body: "Changed the route layout, no table here.", - }); - await repositoriesModule.markPullRequestVisualCaptureSatisfied(env, "JSONbored/gittensory", 58, "vis58"); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // A web-visible route file (isVisualPath) — this is what makes screenshotsAllowed's file-touch gate open - // and buildCapture actually run, on TOP of the no-body-table screenshotTableGate scope match (label). - if (url.includes("/pulls/58/files")) return Response.json([{ filename: "apps/gittensory-ui/src/routes/app.index.tsx", status: "modified", additions: 5, deletions: 1, changes: 6, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/58/reviews")) return Response.json([]); - if (url.includes("/pulls/58/commits")) return Response.json([]); - if (url.endsWith("/pulls/58") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 58, state: "closed" }); } - if (url.endsWith("/pulls/58")) return Response.json({ number: 58, state: "open", user: { login: "visual-contributor" }, head: { sha: "vis58" }, mergeable_state: "clean" }); - // Deployments API: none found -> buildCapture falls through to findPreviewUrlFromChecks below. - if (url.includes("/deployments?")) return Response.json([]); - // Combined status: empty statuses[] (byte-identical to the sibling "closed deterministically" fixture's - // CI stub) -- findPreviewUrlFromChecks' status lookup finds nothing here and falls through to check-runs. - if (url.includes("/commits/vis58/status")) return Response.json({ state: "success", statuses: [] }); - // A completed, successful check-run whose details_url is a real workers.dev preview link -- - // findPreviewUrlFromChecks' SECOND lookup resolves it, and reduceLiveCiAggregate reads it as an ordinary - // green check (no pending/failing signal), so CI still evaluates "passed". - if (url.includes("/commits/vis58/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "preview-deploy", status: "completed", conclusion: "success", details_url: "https://pr-58-preview.workers.dev" }] }); - // Check-suite hardening: reduceLiveCiAggregate only certifies a commit settled once it can ALSO read the - // check-suites (a non-empty check-runs list makes it fetch this as a backstop) -- an unstubbed 404 here - // would fail CLOSED to "pending" and defer the whole review before it ever reaches the publish/maintain - // pass. An empty list means nothing is still running. - if (url.includes("/commits/vis58/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/issues/58/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/58/comments")) return Response.json([]); - // The unified-comment path also creates/patches the "Gittensory Orb Review Agent" check run and applies - // the title-derived type label -- neither is under test here, but both must resolve so the review - // completes normally instead of throwing on an unstubbed 404. - if (url.endsWith("/labels") && method === "POST") return Response.json([]); - if (url.endsWith("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "screenshot-table-bot-capture", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 58, - title: "Update the app index route", - state: "open", - user: { login: "visual-contributor" }, - head: { sha: "vis58" }, - labels: [{ name: "visual" }], - body: "Changed the route layout, no table here.", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - - // The bot's own capture already proved the change visually -- no close, despite no body table at all. - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBe(0); - // The marker persisted and round-trips through toPullRequestRecordFromRow. - const stored = await getPullRequest(env, "JSONbored/gittensory", 58); - expect(stored?.visualCaptureSatisfiedSha).toBe("vis58"); - }); - - // #4110 fail-safe: same fixture as the sibling "satisfies the gate on its own" test above (successful capture, - // in-scope, no body table), except the persistence write itself fails. Proves (1) the write failure never - // throws / never blocks the rest of the review (the marker write is wrapped in its own .catch), and (2) with - // NOTHING persisted, the screenshot-table gate correctly falls back to requiring a body table -- so this - // particular PR IS closed, unlike its sibling. Together the two tests pin both sides of the write's outcome. - it("screenshot-table gate (#4110): a failed visual-capture-satisfied write is swallowed (fail-safe) -- the gate falls back to requiring a body table", async () => { - const markSpy = vi.spyOn(repositoriesModule, "markPullRequestVisualCaptureSatisfied").mockRejectedValueOnce(new Error("D1 write failed")); - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", - GITTENSORY_REVIEW_SCREENSHOTS: "true", - }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto", label: "auto" }, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { screenshotTableGate: { enabled: true, whenLabels: ["visual"] } } }, "repo_file"); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/59/files")) return Response.json([{ filename: "apps/gittensory-ui/src/routes/app.index.tsx", status: "modified", additions: 5, deletions: 1, changes: 6, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/59/reviews")) return Response.json([]); - if (url.includes("/pulls/59/commits")) return Response.json([]); - if (url.endsWith("/pulls/59") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 59, state: "closed" }); } - if (url.endsWith("/pulls/59")) return Response.json({ number: 59, state: "open", user: { login: "visual-contributor" }, head: { sha: "vis59" }, mergeable_state: "clean" }); - if (url.includes("/deployments?")) return Response.json([]); - if (url.includes("/commits/vis59/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/commits/vis59/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "preview-deploy", status: "completed", conclusion: "success", details_url: "https://pr-59-preview.workers.dev" }] }); - if (url.includes("/commits/vis59/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/issues/59/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/59/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/59/comments")) return Response.json([]); - if (url.endsWith("/labels") && method === "POST") return Response.json([]); - if (url.endsWith("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "screenshot-table-bot-capture-write-fail", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 59, - title: "Update the app index route", - state: "open", - user: { login: "visual-contributor" }, - head: { sha: "vis59" }, - labels: [{ name: "visual" }], - body: "Changed the route layout, no table here.", - mergeable_state: "clean", - reviewDecision: "APPROVED", - }, - }, - }); - } finally { - markSpy.mockRestore(); - } - - // The write failure never throws / never blocks the review -- but with nothing persisted, the gate has no - // bot-capture evidence and falls back to its ordinary no-table close. - expect(seen.closed).toBe(true); - const stored = await getPullRequest(env, "JSONbored/gittensory", 59); - expect(stored?.visualCaptureSatisfiedSha).toBeNull(); - }); - - describe("live migrations/** collision recheck (#2550)", () => { - // Full merge-eligible stub set (clean + green + approved), reused across scenarios — a positive test proves - // the collision hold actually suppresses what would otherwise merge; a negative test proves the check - // correctly stays out of the way. `liveTree` is the live git/trees response for `main` (the collision - // source of truth); `seen.treeCalls` counts how many times it was fetched, so the "no latency for a - // non-migrations PR" and "off by default" requirements are directly assertable, not just inferred. - function stubMigrationRecheckFetch(prNumber: number, changedFile: { filename: string; status: string } | Array<{ filename: string; status: string }>, liveTree: Array<{ type: string; path: string }> | "error", seen: { closed: boolean; merged: boolean; labels: string[]; comments: string[]; treeCalls: number }) { - const changedFiles = Array.isArray(changedFile) ? changedFile : [changedFile]; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = (init?.method ?? "GET").toUpperCase(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes(`/git/trees/main`)) { - seen.treeCalls += 1; - if (liveTree === "error") return new Response("not found", { status: 404 }); - return Response.json({ tree: liveTree }); - } - if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes(`/pulls/${prNumber}/`)) { - return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); - } - if (url.includes(`/pulls/${prNumber}/files`)) return Response.json(changedFiles.map((f) => ({ ...f, additions: 5, deletions: 0, changes: 5, patch: "@@\n+ALTER TABLE t ADD COLUMN c TEXT;" }))); - if (url.includes(`/commits/sha1/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes(`/commits/sha1/status`)) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); - if (url.includes(`/commits/sha1/check-suites`)) return Response.json({ check_suites: [] }); - if (url.includes("/branches/")) return Response.json({ contexts: [] }); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes(`/pulls/${prNumber}/merge`) && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true, sha: "merged-sha1" }); - } - if (url.includes(`/pulls/${prNumber}`) && method === "PATCH") { - seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; - return Response.json({ number: prNumber, state: "closed" }); - } - if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); // repo-level label creation (createMissingLabel probe) - if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { - seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - return Response.json({}); - }); - } - - async function seedMigrationRecheckRepo(env: Env, prNumber: number, opts: { premergeContentRecheck?: boolean } = {}) { - await upsertInstallation(env, { - installation: { id: 123, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { contents: "write", pull_requests: "write", issues: "write" }, events: [] }, - }); - await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: { merge: "auto", review_state_label: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - if (opts.premergeContentRecheck !== undefined) { - await upsertRepoFocusManifest(env, "owner/repo", { gate: { premergeContentRecheck: opts.premergeContentRecheck } }); - } - await upsertPullRequestFromGitHub(env, "owner/repo", { number: prNumber, title: "Migration PR", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main" }, labels: [], body: "" }); - } - - it("holds a would-otherwise-merge PR when the live base has a colliding migration number, with the distinct label + rebase comment", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 60, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - stubMigrationRecheckFetch(60, { filename: "migrations/0099_a.sql", status: "added" }, [{ type: "blob", path: "migrations/0099_b.sql" }], seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-collision-hold", repoFullName: "owner/repo", prNumber: 60, installationId: 123 }); - - expect(seen.merged).toBe(false); - expect(seen.closed).toBe(false); // held, never closed — this is a hold, not a close - expect(seen.labels).toContain("migration-collision"); - expect(seen.comments.some((c) => c.includes("rebase") && c.includes("0099"))).toBe(true); - expect(seen.treeCalls).toBe(1); - }); - - it("does not hold when the base has no colliding number — merges normally", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 61, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - stubMigrationRecheckFetch(61, { filename: "migrations/0099_a.sql", status: "added" }, [{ type: "blob", path: "migrations/0050_unrelated.sql" }], seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-no-collision", repoFullName: "owner/repo", prNumber: 61, installationId: 123 }); - - expect(seen.merged).toBe(true); - expect(seen.labels).not.toContain("migration-collision"); - expect(seen.treeCalls).toBe(1); - }); - - it("pays zero latency (never fetches the live tree) for a PR that does not touch migrations/**, even with the recheck enabled", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 62, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - stubMigrationRecheckFetch(62, { filename: "src/index.ts", status: "modified" }, [{ type: "blob", path: "migrations/0099_b.sql" }], seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-not-touched", repoFullName: "owner/repo", prNumber: 62, installationId: 123 }); - - expect(seen.treeCalls).toBe(0); // path-gated — never even attempted the live fetch - expect(seen.merged).toBe(true); - }); - - it("is off by default — never fetches the live tree even for a migrations/**-touching PR when unconfigured", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 63); // premergeContentRecheck left unset — defaults off - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - stubMigrationRecheckFetch(63, { filename: "migrations/0099_a.sql", status: "added" }, [{ type: "blob", path: "migrations/0099_b.sql" }], seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-recheck-off", repoFullName: "owner/repo", prNumber: 63, installationId: 123 }); - - expect(seen.treeCalls).toBe(0); - expect(seen.merged).toBe(true); // a live collision exists but the feature is off — merges anyway (opt-in) - }); - - it("fails OPEN (merges normally) when the live tree fetch errors — never holds a PR on inconclusive data", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 64, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - stubMigrationRecheckFetch(64, { filename: "migrations/0099_a.sql", status: "added" }, "error", seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-fetch-error", repoFullName: "owner/repo", prNumber: 64, installationId: 123 }); - - expect(seen.treeCalls).toBe(1); - expect(seen.merged).toBe(true); - expect(seen.labels).not.toContain("migration-collision"); - }); - - it("fails OPEN (never fetches the live tree) when the PR has no resolvable base ref", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { contents: "write", pull_requests: "write", issues: "write" }, events: [] }, - }); - // No default_branch on the repo record AND no base.ref on the PR record — baseRef resolves to undefined. - await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: { merge: "auto", review_state_label: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertRepoFocusManifest(env, "owner/repo", { gate: { premergeContentRecheck: true } }); - await upsertPullRequestFromGitHub(env, "owner/repo", { number: 65, title: "No base ref", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, labels: [], body: "" }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - stubMigrationRecheckFetch(65, { filename: "migrations/0099_a.sql", status: "added" }, [{ type: "blob", path: "migrations/0099_b.sql" }], seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-no-base-ref", repoFullName: "owner/repo", prNumber: 65, installationId: 123 }); - - expect(seen.treeCalls).toBe(0); // no live target to compare against — never even attempted the fetch - expect(seen.merged).toBe(true); - expect(seen.labels).not.toContain("migration-collision"); - }); - - it("REGRESSION: is deliberately UNCACHED — a live tree that changes between two consecutive maintenance passes is picked up fresh, never served stale", async () => { - // The exact race a caching layer would reintroduce: a sibling PR (not modeled directly here — simulated - // by the live tree response CHANGING between the two fetches, the same effect a sibling merge has) adds - // a colliding migration file in the window between two maintenance passes on the SAME PR. A cache keyed - // by repo+baseRef would serve the first (pre-collision) snapshot on the second pass and miss the - // collision entirely — this asserts both passes fetch fresh and the second one correctly detects it. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 66, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - let liveTree: Array<{ type: string; path: string }> = []; // pass 1: main has nothing colliding yet - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = (init?.method ?? "GET").toUpperCase(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes("/git/trees/main")) { - seen.treeCalls += 1; - return Response.json({ tree: liveTree }); - } - if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/66/")) { - return Response.json({ number: 66, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); - } - if (url.includes("/pulls/66/files")) return Response.json([{ filename: "migrations/0099_a.sql", status: "added", additions: 5, deletions: 0, changes: 5, patch: "@@\n+ALTER TABLE t ADD COLUMN c TEXT;" }]); - if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); - if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/branches/")) return Response.json({ contexts: [] }); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/pulls/66/merge") && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true, sha: "merged-sha1" }); - } - if (url.includes("/issues/66/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/66/labels") && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes("/issues/66/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-fresh-pass-1", repoFullName: "owner/repo", prNumber: 66, installationId: 123 }); - expect(seen.merged).toBe(true); // pass 1: no collision yet — merges - - // Between passes, a sibling PR merges its own colliding 0099 file — main's live tree now has it. - liveTree = [{ type: "blob", path: "migrations/0099_b.sql" }]; - seen.merged = false; - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-fresh-pass-2", repoFullName: "owner/repo", prNumber: 66, installationId: 123 }); - - expect(seen.treeCalls).toBe(2); // every pass fetches fresh — no cache could ever mask the change - expect(seen.labels).toContain("migration-collision"); - expect(seen.merged).toBe(false); // pass 2 correctly catches the now-live collision, never stale-served - }); - - it("does NOT hold for a pre-existing collision between two OTHER files unrelated to this PR's own migration number", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 67, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - // main already has a real collision at 0050 (two unrelated, already-merged files) — nothing to do with - // this PR's own migration at 0099. The prNumbers scoping must exclude it: main is already broken by - // someone else's mistake, but that must not hold an unrelated third PR. - stubMigrationRecheckFetch(67, { filename: "migrations/0099_a.sql", status: "added" }, [ - { type: "blob", path: "migrations/0050_x.sql" }, - { type: "blob", path: "migrations/0050_y.sql" }, - ], seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-unrelated-collision", repoFullName: "owner/repo", prNumber: 67, installationId: 123 }); - - expect(seen.merged).toBe(true); - expect(seen.labels).not.toContain("migration-collision"); - }); - - it("holds and reports every colliding number when a PR touches two migration files that each independently collide", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 68, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - stubMigrationRecheckFetch( - 68, - [ - { filename: "migrations/0098_a.sql", status: "added" }, - { filename: "migrations/0099_a.sql", status: "added" }, - ], - [ - { type: "blob", path: "migrations/0098_b.sql" }, - { type: "blob", path: "migrations/0099_b.sql" }, - ], - seen, - ); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-multi-collision", repoFullName: "owner/repo", prNumber: 68, installationId: 123 }); - - expect(seen.merged).toBe(false); - expect(seen.labels).toContain("migration-collision"); - expect(seen.comments.some((c) => c.includes("0098") && c.includes("0099"))).toBe(true); - }); - - it("does not hold when the live base already contains one of the real grandfathered duplicate pairs, unrelated to this PR's own migration number", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 69, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - // The real, already-shipped 0090 grandfathered pair (see KNOWN_MIGRATION_DUPLICATES) is present on main — - // this must never trigger a hold for an unrelated PR touching a different number. - stubMigrationRecheckFetch(69, { filename: "migrations/0099_a.sql", status: "added" }, [ - { type: "blob", path: "migrations/0090_contributor_cap_label.sql" }, - { type: "blob", path: "migrations/0090_pull_request_detail_sync_head_sha.sql" }, - ], seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-grandfathered", repoFullName: "owner/repo", prNumber: 69, installationId: 123 }); - - expect(seen.merged).toBe(true); - expect(seen.labels).not.toContain("migration-collision"); - }); - - it("REGRESSION: renaming this PR's own not-yet-merged migration file (e.g. a typo fix, same number) does NOT self-collide", async () => { - // Before the fix, prMigrationFilenames was derived from changedPathsForGuardrail's collapsed set, which - // includes BOTH a renamed file's old and new name — counting one logical file as two and self-colliding. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 70, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = (init?.method ?? "GET").toUpperCase(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes("/git/trees/main")) { - seen.treeCalls += 1; - return Response.json({ tree: [] }); // empty live base — nothing else to collide with - } - if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/70/")) { - return Response.json({ number: 70, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); - } - // A single GitHub PR-files entry for a rename: status="renamed", filename=new name, previous_filename=old name. - if (url.includes("/pulls/70/files")) return Response.json([{ filename: "migrations/0099_add_column.sql", previous_filename: "migrations/0099_add_colum.sql", status: "renamed", additions: 1, deletions: 1, changes: 2, patch: "@@\n rename" }]); - if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); - if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/branches/")) return Response.json({ contexts: [] }); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/pulls/70/merge") && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true, sha: "merged-sha1" }); - } - if (url.includes("/issues/70/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/70/labels") && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes("/issues/70/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-rename-self", repoFullName: "owner/repo", prNumber: 70, installationId: 123 }); - - expect(seen.merged).toBe(true); // no false self-collision from counting the old+new rename names as two files - expect(seen.labels).not.toContain("migration-collision"); - }); - - it("REGRESSION: renaming an EXISTING base migration (same number) does NOT self-collide with its own old name still live on main", async () => { - // Before the fix, liveFilenames (fetched from main, which still has the pre-rename name until this PR - // merges) was unioned as-is with prMigrationFilenames (the new name only) — so a same-number typo-fix - // rename of an ALREADY-MERGED base migration counted as two distinct files at one number and - // self-collided, even though the merged tree would only ever contain the renamed file. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 73, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = (init?.method ?? "GET").toUpperCase(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes("/git/trees/main")) { - seen.treeCalls += 1; - // main still has the PRE-rename name — this PR's rename hasn't merged yet. - return Response.json({ tree: [{ type: "blob", path: "migrations/0099_old.sql" }] }); - } - if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/73/")) { - return Response.json({ number: 73, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); - } - // Renames an EXISTING base migration (same number 0099), not a file this PR itself added. - if (url.includes("/pulls/73/files")) return Response.json([{ filename: "migrations/0099_new.sql", previous_filename: "migrations/0099_old.sql", status: "renamed", additions: 1, deletions: 1, changes: 2, patch: "@@\n rename" }]); - if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); - if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/branches/")) return Response.json({ contexts: [] }); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/pulls/73/merge") && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true, sha: "merged-sha1" }); - } - if (url.includes("/issues/73/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/73/labels") && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes("/issues/73/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-rename-existing-base", repoFullName: "owner/repo", prNumber: 73, installationId: 123 }); - - expect(seen.merged).toBe(true); // the pre-rename name still live on main must not count against this PR - expect(seen.labels).not.toContain("migration-collision"); - }); - - it("REGRESSION: renumbering (renaming) this PR's migration to resolve a real collision does not leave a stale hold from the old filename", async () => { - // Before the fix, the stale previousFilename (the OLD number) stayed in prMigrationFilenames forever, - // colliding with an unrelated already-merged file at that old number and permanently re-holding a PR - // that had already fixed itself — exactly the remediation this feature's own comment recommends. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 71, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = (init?.method ?? "GET").toUpperCase(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes("/git/trees/main")) { - seen.treeCalls += 1; - // main already has an unrelated, already-merged file at the OLD number (0099) — nothing to do with - // this PR anymore, since it renumbered away from 0099 to 0100. - return Response.json({ tree: [{ type: "blob", path: "migrations/0099_other_already_merged.sql" }] }); - } - if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/71/")) { - return Response.json({ number: 71, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); - } - if (url.includes("/pulls/71/files")) return Response.json([{ filename: "migrations/0100_mine.sql", previous_filename: "migrations/0099_mine.sql", status: "renamed", additions: 1, deletions: 1, changes: 2, patch: "@@\n rename" }]); - if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); - if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/branches/")) return Response.json({ contexts: [] }); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/pulls/71/merge") && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true, sha: "merged-sha1" }); - } - if (url.includes("/issues/71/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/71/labels") && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes("/issues/71/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-renumber-remediation", repoFullName: "owner/repo", prNumber: 71, installationId: 123 }); - - expect(seen.merged).toBe(true); // the stale old-number previousFilename must not re-trigger a hold - expect(seen.labels).not.toContain("migration-collision"); - }); - - it("REGRESSION: deleting this PR's own colliding migration file does not still count it as one of the PR's own filenames", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedMigrationRecheckRepo(env, 72, { premergeContentRecheck: true }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[], treeCalls: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = (init?.method ?? "GET").toUpperCase(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes("/git/trees/main")) { - seen.treeCalls += 1; - return Response.json({ tree: [{ type: "blob", path: "migrations/0099_other_already_merged.sql" }] }); - } - if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes("/pulls/72/")) { - return Response.json({ number: 72, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); - } - // The PR deletes its own migration file (status="removed") — it no longer exists in the PR's tree. - if (url.includes("/pulls/72/files")) return Response.json([{ filename: "migrations/0099_mine.sql", status: "removed", additions: 0, deletions: 5, changes: 5, patch: "@@\n-removed" }]); - if (url.includes("/commits/sha1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/sha1/status")) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); - if (url.includes("/commits/sha1/check-suites")) return Response.json({ check_suites: [] }); - if (url.includes("/branches/")) return Response.json({ contexts: [] }); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/pulls/72/merge") && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true, sha: "merged-sha1" }); - } - if (url.includes("/issues/72/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/72/labels") && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes("/issues/72/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "migration-removed-file", repoFullName: "owner/repo", prNumber: 72, installationId: 123 }); - - // With no migrations/**-touching file left in the PR's own set (the only entry is `status: "removed"`), - // prMigrationFilenames is empty — the whole recheck is path-gated off, so it never even fetches the tree. - expect(seen.treeCalls).toBe(0); - expect(seen.merged).toBe(true); - expect(seen.labels).not.toContain("migration-collision"); - }); - }); - - describe("unlinked-issue guardrail (#unlinked-issue-guardrail, credibility-gate-farming defense)", () => { - // Mirrors the #2550 migration-recheck fixture immediately above: full merge-eligible stub set - // (clean + green + approved) so a positive test proves the hold actually suppresses what would - // otherwise merge, and a negative test proves the guardrail correctly stays out of the way / off by - // default. `run` (the env.AI.run spy) is asserted directly rather than inferred from side effects. - function stubUnlinkedIssueGuardrailFetch(prNumber: number, seen: { closed: boolean; merged: boolean; labels: string[]; comments: string[] }) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = (init?.method ?? "GET").toUpperCase(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); - if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes(`/pulls/${prNumber}/`)) { - return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] }); - } - // src/github/webhook.ts (not src/queue/**): this block tests the unlinked-issue guardrail specifically, - // and src/queue/** is one of ENGINE_DECISION_GUARDRAIL_GLOBS' built-in invariants (guardrail-config.ts) — - // a diff touching it would unconditionally hold regardless of this guardrail's own on/off setting. - if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([{ filename: "src/github/webhook.ts", status: "modified", additions: 5, deletions: 0, changes: 5, patch: "@@\n+dedupe retries" }]); - if (url.includes(`/commits/sha1/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes(`/commits/sha1/status`)) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); - if (url.includes(`/commits/sha1/check-suites`)) return Response.json({ check_suites: [] }); - if (url.includes("/branches/")) return Response.json({ contexts: [] }); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes(`/pulls/${prNumber}/merge`) && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true, sha: "merged-sha1" }); - } - if (url.includes(`/pulls/${prNumber}`) && method === "PATCH") { - seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; - return Response.json({ number: prNumber, state: "closed" }); - } - if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { - seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); - return Response.json({ id: 1 }, { status: 201 }); - } - if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - return Response.json({}); - }); - } - - async function seedGuardrailRepo(env: Env, prNumber: number, opts: { guardrailMode?: "hold" | "off"; prBody?: string; autonomy?: Record } = {}) { - await upsertInstallation(env, { - installation: { id: 123, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { contents: "write", pull_requests: "write", issues: "write" }, events: [] }, - }); - await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "owner/repo", autonomy: opts.autonomy ?? { merge: "auto", review_state_label: "auto" }, aiReviewMode: "off", gatePack: "oss-anti-slop", gateCheckMode: "enabled", reviewCheckMode: "required", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - if (opts.guardrailMode !== undefined) { - await upsertRepoFocusManifest(env, "owner/repo", { settings: { unlinkedIssueGuardrail: { mode: opts.guardrailMode } } }); - } - await upsertIssueFromGitHub(env, "owner/repo", { number: 5, title: "webhook retry duplicate bug report", state: "open", user: { login: "someone" }, labels: [], body: "retries duplicate events under heavy load, needs a dedup key" }); - await upsertPullRequestFromGitHub(env, "owner/repo", { number: prNumber, title: "fix webhook retry duplicate bug", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main" }, labels: [], body: opts.prBody ?? "" }); - } - - it("holds a would-otherwise-merge PR when its diff appears to directly solve an existing open issue it never linked", async () => { - const run = vi.fn(async () => ({ response: JSON.stringify({ matched: true, confidence: 0.9, evidence: "adds the missing dedup key" }) })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai }); - await seedGuardrailRepo(env, 80, { guardrailMode: "hold" }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; - stubUnlinkedIssueGuardrailFetch(80, seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-hold", repoFullName: "owner/repo", prNumber: 80, installationId: 123 }); - - expect(seen.merged).toBe(false); - expect(seen.closed).toBe(false); // held, never closed — this is a hold, not a close - expect(seen.labels).toContain("manual-review"); - expect(seen.comments.some((c) => c.includes("#5"))).toBe(true); - expect(run).toHaveBeenCalled(); - }); - - it("is off by default — never calls the AI even for a PR whose diff clearly overlaps an open issue", async () => { - const run = vi.fn(async () => ({ response: JSON.stringify({ matched: true, confidence: 0.9, evidence: "x" }) })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai }); - await seedGuardrailRepo(env, 81); // guardrailMode left unset — defaults off - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; - stubUnlinkedIssueGuardrailFetch(81, seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-off", repoFullName: "owner/repo", prNumber: 81, installationId: 123 }); - - expect(run).not.toHaveBeenCalled(); - expect(seen.merged).toBe(true); - }); - - it("does not call the AI when the PR already links an issue, even with the guardrail on", async () => { - const run = vi.fn(async () => ({ response: JSON.stringify({ matched: true, confidence: 0.9, evidence: "x" }) })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai }); - await seedGuardrailRepo(env, 82, { guardrailMode: "hold", prBody: "Closes #5" }); - const seen = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; - stubUnlinkedIssueGuardrailFetch(82, seen); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-already-linked", repoFullName: "owner/repo", prNumber: 82, installationId: 123 }); - - expect(run).not.toHaveBeenCalled(); - expect(seen.merged).toBe(true); - }); - - it("escalates to a CLOSE on a second confirmed match by the same contributor (#unlinked-issue-guardrail-followup)", async () => { - const run = vi.fn(async () => ({ response: JSON.stringify({ matched: true, confidence: 0.9, evidence: "adds the missing dedup key" }) })); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai }); - - await seedGuardrailRepo(env, 90, { guardrailMode: "hold" }); - const seenFirst = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; - stubUnlinkedIssueGuardrailFetch(90, seenFirst); - await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-repeat-first", repoFullName: "owner/repo", prNumber: 90, installationId: 123 }); - expect(seenFirst.closed).toBe(false); // first confirmed match: held, not closed - expect(seenFirst.merged).toBe(false); - - // The second PR needs `close` autonomy acting for the escalated disposition to actually execute as a - // close (the first PR's hold path only ever needs `merge`/`review_state_label`). - await seedGuardrailRepo(env, 91, { guardrailMode: "hold", autonomy: { merge: "auto", review_state_label: "auto", close: "auto" } }); - const seenSecond = { closed: false, merged: false, labels: [] as string[], comments: [] as string[] }; - stubUnlinkedIssueGuardrailFetch(91, seenSecond); - await processJob(env, { type: "agent-regate-pr", deliveryId: "unlinked-issue-repeat-second", repoFullName: "owner/repo", prNumber: 91, installationId: 123 }); - expect(seenSecond.closed).toBe(true); // same contributor's SECOND confirmed match: closed - expect(seenSecond.merged).toBe(false); - }); - }); - - describe("force-fresh-rebase-before-merge gate (#2552)", () => { - // Full merge-eligible stub set (clean + green + approved), reused across scenarios — mirrors the #2550 - // migration-recheck fixture above. `baseAdvancedAt` stubs the NEW /commits/{baseRef} freshness read; - // `null` simulates an unreadable base commit (404). - function stubFreshRebaseFetch(prNumber: number, opts: { baseAdvancedAt: string | null; mergeableState?: string; headSha?: string }, seen: { merged: boolean; updateBranchCalls: number; baseCommitCalls: number }) { - const headSha = opts.headSha ?? "sha1"; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = (init?.method ?? "GET").toUpperCase(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes(`/pulls/${prNumber}/update-branch`) && method === "PUT") { - seen.updateBranchCalls += 1; - return Response.json({ message: "Updating pull request branch." }, { status: 202 }); - } - if (url.endsWith("/commits/main")) { - seen.baseCommitCalls += 1; - if (opts.baseAdvancedAt === null) return new Response("not found", { status: 404 }); - return Response.json({ commit: { committer: { date: opts.baseAdvancedAt } } }); - } - if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes(`/pulls/${prNumber}/`)) { - return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: headSha }, base: { ref: "main", sha: "base" }, mergeable_state: opts.mergeableState ?? "clean", labels: [] }); - } - if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([{ filename: "src/index.ts", status: "modified", additions: 5, deletions: 1, changes: 6, patch: "@@\n+export const x = 1;" }]); - if (url.includes(`/commits/${headSha}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes(`/commits/${headSha}/status`)) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] }); - if (url.includes(`/commits/${headSha}/check-suites`)) return Response.json({ check_suites: [] }); - if (url.includes("/branches/")) return Response.json({ contexts: [] }); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes(`/pulls/${prNumber}/merge`) && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true, sha: "merged-sha1" }); - } - if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") return Response.json([]); - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - return Response.json({}); - }); - } - - async function seedFreshRebaseRepo(env: Env, prNumber: number, opts: { requireFreshRebaseWindowMinutes?: number | null; autonomy?: Record } = {}) { - await upsertInstallation(env, { - installation: { id: 123, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { contents: "write", pull_requests: "write", issues: "write" }, events: [] }, - }); - await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "owner/repo", - autonomy: opts.autonomy ?? { merge: "auto", update_branch: "auto", label: "auto" }, - autoMaintain: { requireApprovals: 0, mergeMethod: "squash" }, - aiReviewMode: "off", - gatePack: "oss-anti-slop", - gateCheckMode: "enabled", reviewCheckMode: "required", - checkRunMode: "off", - commentMode: "off", - publicSurface: "off", - ...(opts.requireFreshRebaseWindowMinutes !== undefined ? { requireFreshRebaseWindowMinutes: opts.requireFreshRebaseWindowMinutes } : {}), - }); - await upsertPullRequestFromGitHub(env, "owner/repo", { number: prNumber, title: "Fresh rebase PR", state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main" }, labels: [], body: "" }); - } - - it("merges normally when the base has not advanced recently", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedFreshRebaseRepo(env, 90, { requireFreshRebaseWindowMinutes: 10 }); - const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; - stubFreshRebaseFetch(90, { baseAdvancedAt: new Date(Date.now() - 60 * 60_000).toISOString() }, seen); // 1h ago, outside a 10m window - - await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-old-base", repoFullName: "owner/repo", prNumber: 90, installationId: 123 }); - - expect(seen.baseCommitCalls).toBe(1); - expect(seen.updateBranchCalls).toBe(0); - expect(seen.merged).toBe(true); - const merge = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.merge").first<{ outcome: string }>(); - expect(merge?.outcome).toBe("completed"); - }); - - it("forces update_branch instead of merging when the base advanced within the freshness window", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedFreshRebaseRepo(env, 91, { requireFreshRebaseWindowMinutes: 10 }); - const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; - stubFreshRebaseFetch(91, { baseAdvancedAt: new Date(Date.now() - 60_000).toISOString() }, seen); // 1 minute ago, within a 10m window - - await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-forced", repoFullName: "owner/repo", prNumber: 91, installationId: 123 }); - - expect(seen.updateBranchCalls).toBe(1); - expect(seen.merged).toBe(false); - const ub = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.update_branch").first<{ outcome: string }>(); - expect(ub?.outcome).toBe("completed"); - const forced = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.forced_rebase_freshness").first<{ outcome: string }>(); - expect(forced?.outcome).toBe("completed"); - const merge = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.merge").first<{ n: number }>(); - expect(merge?.n).toBe(0); - }); - - it("never fetches the base commit or forces a rebase when the setting is unset (off by default)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedFreshRebaseRepo(env, 92); // requireFreshRebaseWindowMinutes left unset - const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; - stubFreshRebaseFetch(92, { baseAdvancedAt: new Date().toISOString() }, seen); // "now" — would force if the setting were on - - await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-off", repoFullName: "owner/repo", prNumber: 92, installationId: 123 }); - - expect(seen.baseCommitCalls).toBe(0); - expect(seen.updateBranchCalls).toBe(0); - expect(seen.merged).toBe(true); - }); - - it("falls through to a normal merge once the bounded-retry cap is reached, with a cap-exceeded audit event", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedFreshRebaseRepo(env, 93, { requireFreshRebaseWindowMinutes: 10 }); - // Seed the bounded-retry counter at the cap (3) for this repo+PR, matching what 3 prior forced - // attempts would have left behind. - await env.SELFHOST_TRANSIENT_CACHE?.set("fresh-rebase-forced:owner/repo#93", "3", 24 * 3600); - const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; - stubFreshRebaseFetch(93, { baseAdvancedAt: new Date(Date.now() - 60_000).toISOString() }, seen); // still within window - - await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-capped", repoFullName: "owner/repo", prNumber: 93, installationId: 123 }); - - expect(seen.updateBranchCalls).toBe(0); // capped — never forces a 4th attempt - expect(seen.merged).toBe(true); // falls through to a normal merge instead - const capped = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.fresh_rebase_window_cap_exceeded").first<{ outcome: string }>(); - expect(capped?.outcome).toBe("completed"); - }); - - it("fails open (merges normally) when the base commit is unreadable", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedFreshRebaseRepo(env, 94, { requireFreshRebaseWindowMinutes: 10 }); - const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; - stubFreshRebaseFetch(94, { baseAdvancedAt: null }, seen); // 404 on the base commit fetch - - await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-unreadable", repoFullName: "owner/repo", prNumber: 94, installationId: 123 }); - - expect(seen.baseCommitCalls).toBe(1); - expect(seen.updateBranchCalls).toBe(0); - expect(seen.merged).toBe(true); - }); - - it("falls through to a normal merge when the forced update_branch action itself is not authorized", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - // update_branch is deliberately absent from autonomy (resolves to the deny-by-default "observe" level), - // while merge stays "auto" — proving the freshness gate fails open independently of the eventual merge - // action's own authorization. - await seedFreshRebaseRepo(env, 95, { requireFreshRebaseWindowMinutes: 10, autonomy: { merge: "auto", label: "auto" } }); - const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; - stubFreshRebaseFetch(95, { baseAdvancedAt: new Date(Date.now() - 60_000).toISOString() }, seen); // within window - - await processJob(env, { type: "agent-regate-pr", deliveryId: "fresh-rebase-not-authorized", repoFullName: "owner/repo", prNumber: 95, installationId: 123 }); - - expect(seen.baseCommitCalls).toBe(1); - expect(seen.updateBranchCalls).toBe(0); // denied by autonomy before any GitHub mutation is attempted - expect(seen.merged).toBe(true); // falls through to the normal merge decision - const denied = await env.DB.prepare("select outcome from audit_events where event_type = ? order by created_at desc limit 1").bind("agent.action.update_branch").first<{ outcome: string }>(); - expect(denied?.outcome).toBe("denied"); - }); - - it("REGRESSION (gate finding): the bounded-retry counter accumulates across successful forces even though each one changes the head SHA", async () => { - // A successful update_branch itself produces a NEW head SHA (the merge-base-into-head commit). The - // counter must NOT reset just because ITS OWN action changed the head -- otherwise the cap could never - // be reached via the exact path it exists to bound, and a fast-moving base would force a rebase on - // EVERY pass forever. Simulates 3 rounds, each with a genuinely different head SHA (mirroring the - // synchronize webhook a real update_branch triggers), then a 4th round proving the cap holds. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedFreshRebaseRepo(env, 96, { requireFreshRebaseWindowMinutes: 10 }); - const shas = ["sha-r1", "sha-r2", "sha-r3", "sha-r4"]; - - for (const [round, sha] of shas.entries()) { - await upsertPullRequestFromGitHub(env, "owner/repo", { number: 96, title: "Fresh rebase PR", state: "open", user: { login: "contributor" }, head: { sha }, base: { ref: "main" }, labels: [], body: "" }); - const seen = { merged: false, updateBranchCalls: 0, baseCommitCalls: 0 }; - stubFreshRebaseFetch(96, { baseAdvancedAt: new Date(Date.now() - 60_000).toISOString(), headSha: sha }, seen); // always within window - - await processJob(env, { type: "agent-regate-pr", deliveryId: `fresh-rebase-multi-round-${round}`, repoFullName: "owner/repo", prNumber: 96, installationId: 123 }); - - if (round < 3) { - // Rounds 0-2 (attempts 1-3): still under/at the cap -- forces update_branch, never merges. - expect(seen.updateBranchCalls).toBe(1); - expect(seen.merged).toBe(false); - } else { - // Round 3 (the 4th evaluation): the cap (3) was already reached by round 2 -- falls through to merge. - expect(seen.updateBranchCalls).toBe(0); - expect(seen.merged).toBe(true); - } - } - - const forcedCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.forced_rebase_freshness").first<{ n: number }>(); - expect(forcedCount?.n).toBe(3); // exactly 3 successful forces, not 4 - const capped = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.fresh_rebase_window_cap_exceeded").first<{ outcome: string }>(); - expect(capped?.outcome).toBe("completed"); - }); - }); - - it("contributor open-PR cap (#2270): a contributor's 3rd open PR (over a cap of 2) is labeled + closed deterministically with no merit merge", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "n/a", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // Two PRE-EXISTING open PRs from the same author, seeded directly (as if opened moments earlier). - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - // The label is the configurable `.gittensory.yml` value below — nothing is hard-coded. - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { contributorCapLabel: "spam-cap" } }, "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(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/55/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-close", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - // Deterministic gate: closed + labeled (with the configured label), and the AI was NEVER called. - expect(aiCalls).toBe(0); - expect(seen.closed).toBe(true); - expect(seen.labels).toContain("spam-cap"); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - // No merit merge despite a clean+green+approved PR (the cap short-circuits ahead of merit). - const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); - expect(mergeAudit?.n).toBe(0); - // The close comment states the cap + current count (public, unlike the blacklist's static-only comment). - expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open pull requests") && c.includes("limit of 2"))).toBe(true); - }); - - it("contributor open-PR cap (#2270): a maintainer-named autoCloseExemptLogins entry is exempt from the PER-REPO cap too (not just the install-wide cap)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // Two PRE-EXISTING open PRs from an exempt bot author (e.g. a third-party automation App like Sentry's Seer - // fix bot) — same over-cap shape as the "3rd PR" test above, but this login is on autoCloseExemptLogins. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Sentry fix one", state: "open", user: { login: "sentry[bot]" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Sentry fix two", state: "open", user: { login: "sentry[bot]" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - 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(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "sentry[bot]" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/55/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-exempt-login", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Sentry's 3rd PR", state: "open", user: { login: "sentry[bot]" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - // Exempt: the 3rd PR is NOT closed or labeled for the cap, despite being (numerically) over it. - expect(seen.closed).toBe(false); - expect(seen.labels).not.toContain("over-contributor-limit"); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBe(0); - }); - - function stubContributorCapCiCancelFetch(seen: { closed: boolean; cancelledIds: number[]; listedStatuses: string[] }, runListResponses: { in_progress?: number[]; queued?: number[] } = {}, cancelResponse: () => Response = () => new Response(null, { status: 202 })) { - return async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - // The other-siblings live-state recheck (#2270 complete-set fix) confirms every counted sibling PR is - // still open before trusting it toward the cap — farmer99's two pre-existing PRs (53, 54) must report open. - if (url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) return Response.json({ number: 53, state: "open" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels")) return Response.json([]); - if (url.includes("/issues/55/comments")) return Response.json([]); - if (url.includes("/actions/runs?head_sha=f55&status=in_progress")) { seen.listedStatuses.push("in_progress"); return Response.json({ workflow_runs: (runListResponses.in_progress ?? []).map((id) => ({ id, event: "pull_request", pull_requests: [{ number: 55 }] })) }); } - if (url.includes("/actions/runs?head_sha=f55&status=queued")) { seen.listedStatuses.push("queued"); return Response.json({ workflow_runs: (runListResponses.queued ?? []).map((id) => ({ id, event: "pull_request", pull_requests: [{ number: 55 }] })) }); } - if (url.includes("/actions/runs/") && url.endsWith("/cancel") && method === "POST") { - seen.cancelledIds.push(Number(url.match(/\/actions\/runs\/(\d+)\/cancel/)?.[1])); - return cancelResponse(); - } - return Response.json({}); - }; - } - - it("contributor open-PR cap (#2462): a contributor_cap close cancels the PR's in-flight CI runs when contributorCapCancelCi is enabled", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: true, - }); - const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; - vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [101], queued: [102] })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-cancel-ci-enabled", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.listedStatuses.sort()).toEqual(["in_progress", "queued"]); - expect(seen.cancelledIds.sort()).toEqual([101, 102]); - const cancelAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.contributor_cap_ci_cancelled'").first<{ n: number }>(); - expect(cancelAudit?.n).toBeGreaterThanOrEqual(1); - }); - - it("contributor open-PR cap (#2462): a failing cancel-success audit write does not throw — the close still completes", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - 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; - const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { - if (event.eventType === "github_app.contributor_cap_ci_cancelled") throw new Error("audit DB down"); - await originalRecordAuditEvent(auditEnv, event); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-cancel-ci-audit-fail", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }), - ).resolves.toBeUndefined(); - auditSpy.mockRestore(); - expect(seen.closed).toBe(true); - }); - - it("contributor open-PR cap (#2462): a failing cancel-FAILURE audit write also does not throw — the close still completes", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: true, - }); - const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; - vi.stubGlobal( - "fetch", - stubContributorCapCiCancelFetch(seen, { in_progress: [104] }, () => new Response(null, { status: 500 })), - ); - const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; - const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { - if (event.eventType === "github_app.contributor_cap_ci_cancel_failed") throw new Error("audit DB down"); - await originalRecordAuditEvent(auditEnv, event); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-cancel-ci-failed-audit-fail", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }), - ).resolves.toBeUndefined(); - auditSpy.mockRestore(); - expect(seen.closed).toBe(true); - }); - - it("contributor open-PR cap (#2462): contributorCapCancelCi unset (default) never attempts to cancel CI runs", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - // contributorCapCancelCi intentionally omitted — off by default, no CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT set. - }); - const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; - vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [201] })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-cancel-ci-off", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.listedStatuses).toEqual([]); - expect(seen.cancelledIds).toEqual([]); - }); - - it("contributor open-PR cap (#2462): a missing actions:write permission degrades gracefully — the close still succeeds and a permission_missing audit is recorded", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - 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(); - const method = init?.method ?? "GET"; - if (url.includes("/actions/runs?head_sha=")) return Response.json({ message: "Resource not accessible by integration" }, { status: 403 }); - return stubContributorCapCiCancelFetch(seen)(input, init); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-cancel-ci-permission-missing", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - // The close itself still succeeded and is recorded "completed", NOT "error" -- the cancel-permission gap - // must never retroactively fail an already-successful close (#2462 core requirement). - expect(seen.closed).toBe(true); - const closeAudit = await env.DB.prepare("select outcome from audit_events where event_type = 'agent.action.close' order by created_at desc limit 1").first<{ outcome: string }>(); - expect(closeAudit?.outcome).toBe("completed"); - const permissionAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.contributor_cap_ci_cancel_permission_missing'").first<{ n: number }>(); - expect(permissionAudit?.n).toBeGreaterThanOrEqual(1); - }); - - it("contributor open-PR cap (#2462, #gate finding): a genuine cancel error (not a permission gap) is recorded under its own event type, distinct from permission_missing", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: true, - }); - const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; - vi.stubGlobal( - "fetch", - stubContributorCapCiCancelFetch(seen, { in_progress: [901] }, () => new Response(null, { status: 500 })), - ); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-cancel-ci-generic-error", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(true); // the close itself still succeeds regardless of the cancel outcome - const failedAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.contributor_cap_ci_cancel_failed'").first<{ n: number }>(); - expect(failedAudit?.n).toBeGreaterThanOrEqual(1); - const permissionAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.contributor_cap_ci_cancel_permission_missing'").first<{ n: number }>(); - expect(permissionAudit?.n).toBe(0); // a generic 500 must never be misclassified as a permission gap - }); - - it("contributor open-PR cap (#2462): CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT env var enables cancellation when the repo hasn't configured its own value", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT: "true" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - // contributorCapCancelCi intentionally omitted (null) -- falls back to the env var default above. - }); - const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; - vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [301] })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-cancel-ci-env-default", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.cancelledIds).toEqual([301]); - }); - - it("contributor open-PR cap (#2462): an explicit repo-level contributorCapCancelCi: false overrides a true CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT: "true" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - contributorCapCancelCi: false, - }); - const seen = { closed: false, cancelledIds: [] as number[], listedStatuses: [] as string[] }; - vi.stubGlobal("fetch", stubContributorCapCiCancelFetch(seen, { in_progress: [401] })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-cancel-ci-repo-override", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.listedStatuses).toEqual([]); - expect(seen.cancelledIds).toEqual([]); - }); - - it("contributor open-PR cap (#2270): uses a complete author-scoped set beyond the duplicate-analysis 100-row sample (regression)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - for (let number = 1; number <= 100; number += 1) { - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number, title: `Busy repo PR ${number}`, state: "open", user: { login: `other-${number}` }, head: { sha: `o${number}` }, labels: [], body: "x" }); - } - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 101, title: "Spammer PR one", state: "open", user: { login: "spammer" }, head: { sha: "s101" }, labels: [], body: "x" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 1, - }); - const seen = { closed: false, comments: [] as string[] }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/pulls/101") && method === "GET") return Response.json({ number: 101, state: "open" }); - if (url.includes("/pulls/102/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/102/reviews")) return Response.json([]); - if (url.includes("/pulls/102/commits")) return Response.json([]); - if (url.endsWith("/pulls/102") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 102, state: "closed" }); } - if (url.endsWith("/pulls/102")) return Response.json({ number: 102, state: "open", user: { login: "spammer" }, head: { sha: "s102" }, mergeable_state: "clean" }); - if (url.includes("/commits/s102/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/s102/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/102/labels")) return Response.json([]); - if (url.includes("/issues/102/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } - if (url.includes("/issues/102/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-busy-repo", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 102, title: "Spammer PR two", state: "open", user: { login: "spammer" }, head: { sha: "s102" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.comments.some((c) => c.includes("@spammer") && c.includes("2 open pull requests") && c.includes("limit of 1"))).toBe(true); - }); - - it("REGRESSION (security review finding): the per-repo cap's sibling live-check bounds concurrency instead of firing one request per open PR at once", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // 30 OTHER open PRs from the SAME author — well beyond CONTRIBUTOR_CAP_LIVE_CHECK_CONCURRENCY (10), so an - // unbounded Promise.all would fire all 30 live-state GETs at once. - const SIBLING_COUNT = 30; - for (let number = 1; number <= SIBLING_COUNT; number += 1) { - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number, title: `Prolific PR ${number}`, state: "open", user: { login: "prolific" }, head: { sha: `p${number}` }, labels: [], body: "x" }); - } - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 100, // above SIBLING_COUNT + 1 — this test only cares about concurrency, not closing. - }); - let inFlight = 0; - let maxInFlight = 0; - const siblingCheckPattern = new RegExp(`/pulls/(?:${Array.from({ length: SIBLING_COUNT }, (_, i) => i + 1).join("|")})$`); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (siblingCheckPattern.test(url) && method === "GET") { - inFlight += 1; - maxInFlight = Math.max(maxInFlight, inFlight); - // A tiny real delay forces genuine overlap between concurrently-dispatched sibling checks — without - // it, each mock resolves synchronously and never actually overlaps another in-flight call. - await new Promise((resolve) => setTimeout(resolve, 5)); - inFlight -= 1; - return Response.json({ state: "open" }); - } - if (url.includes(`/pulls/${SIBLING_COUNT + 1}/files`)) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes(`/pulls/${SIBLING_COUNT + 1}/reviews`)) return Response.json([]); - if (url.includes(`/pulls/${SIBLING_COUNT + 1}/commits`)) return Response.json([]); - if (url.endsWith(`/pulls/${SIBLING_COUNT + 1}`)) return Response.json({ number: SIBLING_COUNT + 1, state: "open", user: { login: "prolific" }, head: { sha: `p${SIBLING_COUNT + 1}` }, mergeable_state: "clean" }); - if (url.includes(`/commits/p${SIBLING_COUNT + 1}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes(`/commits/p${SIBLING_COUNT + 1}/status`)) return Response.json({ state: "success", statuses: [] }); - if (url.includes(`/issues/${SIBLING_COUNT + 1}/labels`)) return Response.json([]); - if (url.includes(`/issues/${SIBLING_COUNT + 1}/comments`)) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-bounded-concurrency", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: SIBLING_COUNT + 1, title: "Prolific author's newest PR", state: "open", user: { login: "prolific" }, head: { sha: `p${SIBLING_COUNT + 1}` }, labels: [], body: "x", mergeable_state: "clean" }, - }, - }); - - expect(maxInFlight).toBeGreaterThan(1); // proves the check is genuinely concurrent, not accidentally serial - expect(maxInFlight).toBeLessThanOrEqual(10); // CONTRIBUTOR_CAP_LIVE_CHECK_CONCURRENCY - }); - - it("contributor open-PR cap (#2270): disabled (no cap configured, the default) never closes an over-threshold contributor", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - // No contributorOpenPrCap set — the default, disabled state. - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-disabled", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - expect(seen.closed).toBe(false); - }); - - it("contributor open-PR cap (#2270): a contributor's 2nd PR AT (not over) a cap of 2 is not closed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // Only ONE pre-existing open PR from this author — the incoming PR is their 2nd, exactly at the cap. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-at-limit", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 2nd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - expect(seen.closed).toBe(false); - }); - - it("install-wide contributor open-item cap (#2562): an actor over the install-wide cap but under EVERY individual repo's own cap is still caught", async () => { - // No per-repo contributorOpenPrCap is configured on EITHER repo -- only the install-wide env cap. One - // pre-existing open PR on repo-a and one on repo-b (2 total), plus the incoming 3rd (also on repo-a) = 3, - // over a global cap of 2 -- even though repo-a's own count (2) and repo-b's own count (1) would each - // individually be unremarkable (and no per-repo cap is even configured to catch them). - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - // upsertInstallation's own `repositories:` array is NOT itself persisted to the `repositories` table (only - // the `installations` row) -- the real webhook pipeline registers a repo's installationId as a side effect - // of processing an event FOR that repo, which never happens here for repo-b (the non-webhook-triggered repo). - // Register it explicitly so countOpenItemsForAuthorAcrossRepos's installation-scoped lookup can find its rows. - await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); - await upsertPullRequestFromGitHub(env, "JSONbored/repo-a", { number: 20, title: "Farmer PR on repo-a", state: "open", user: { login: "farmer99" }, head: { sha: "fa20" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/repo-a", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - // Deliberately NO contributorOpenPrCap here — only the install-wide env cap should catch this. - }); - const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - // Install-wide live-verify (#2562 gate-review follow-up) re-fetches every OTHER counted sibling before - // trusting it toward the cap -- both of farmer99's other open items must resolve as confirmed-open here. - if (url.endsWith("/repos/JSONbored/repo-a/pulls/20")) return Response.json({ number: 20, state: "open" }); - if (url.endsWith("/repos/JSONbored/repo-b/pulls/10")) return Response.json({ number: 10, state: "open" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/55/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-cap-close", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.labels).toContain("over-contributor-limit"); - // Install-wide cap counts BOTH open PRs and open issues together (#2562 gate-review follow-up), so the - // close message reports the mixed noun rather than a stale "pull requests"-only phrasing. - expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open pull requests and issues") && c.includes("across every repository it gates"))).toBe(true); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - }); - - it("install-wide contributor open-item cap (#2562): stops live verification after the cap is exceeded", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "1" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }], - }); - for (let number = 1; number <= 30; number += 1) { - await upsertPullRequestFromGitHub(env, "JSONbored/repo-a", { number, title: `Farmer PR ${number}`, state: "open", user: { login: "farmer99" }, head: { sha: `fa${number}` }, labels: [], body: "x" }); - } - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/repo-a", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - }); - const seen = { closed: false, livePullReads: [] as number[] }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - const siblingPull = url.match(/\/repos\/JSONbored\/repo-a\/pulls\/(\d+)$/); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (siblingPull && siblingPull[1] !== "55") { seen.livePullReads.push(Number(siblingPull[1])); return Response.json({ number: Number(siblingPull[1]), state: "open" }); } - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-cap-short-circuit", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 31st PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.livePullReads).toHaveLength(10); - expect(seen.livePullReads).not.toContain(11); - }); - - it("install-wide contributor open-item cap (#2562, #4511): env var unset falls back to the real default (20), so a spread-across-repos actor well under it is not closed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // no GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP -- resolves to the DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP, not "no cap" (#4511) - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); - await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/repo-a", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-cap-off-by-default", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - expect(seen.closed).toBe(false); - }); - - it("install-wide contributor open-item cap (#2562): a maintainer-named autoCloseExemptLogins entry is exempt from the install-wide cap", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); - await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/repo-a", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - autoCloseExemptLogins: ["farmer99"], - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-cap-exempt", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - expect(seen.closed).toBe(false); - }); - - it("install-wide contributor open-item cap (#2562): an author AT (not over) the configured install-wide cap is not closed", async () => { - // Global cap is configured (2) and reached exactly (repo-b's 1 pre-existing + this incoming PR = 2), so the - // install-wide check must fall through without matching -- the `installOpenCount > globalCap` false branch. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); - await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/repo-a", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-cap-at-limit", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 2nd PR, at the install-wide limit", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - expect(seen.closed).toBe(false); - }); - - it("install-wide contributor open-item cap (#4511): a CONFIRMED official Gittensor miner gets the higher miner-specific cap, not the human one, even though the human cap alone would already be exceeded", async () => { - // Human cap (2) would already be exceeded by 3 open items -- but farmer99 resolves as a confirmed miner via - // the /miners API, so the fleet-appropriate default (50, GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER unset) applies - // instead, and 3 is nowhere near that. Must fall through without matching. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); - await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb10" }, labels: [], body: "y" }); - await upsertPullRequestFromGitHub(env, "JSONbored/repo-b", { number: 11, title: "Farmer 2nd PR on repo-b", state: "open", user: { login: "farmer99" }, head: { sha: "fb11" }, labels: [], body: "z" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/repo-a", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "farmer99", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://api.gittensor.io/miners/123") return Response.json({}); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/10") || url.endsWith("/pulls/11")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-cap-confirmed-miner", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Confirmed miner's 3rd PR install-wide", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - expect(seen.closed).toBe(false); - }); - - it("contributor open-PR cap (#2270): the repo OWNER's own PR is never closed even over the cap (live processor path, not just the planner)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Owner PR one", state: "open", user: { login: "JSONbored" }, head: { sha: "o53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Owner PR two", state: "open", user: { login: "JSONbored" }, head: { sha: "o54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "JSONbored" }, head: { sha: "o55" }, mergeable_state: "clean" }); - if (url.includes("/commits/o55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/o55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-owner", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Owner's 3rd PR", state: "open", user: { login: "JSONbored" }, head: { sha: "o55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(seen.closed).toBe(false); - }); - - it("contributor open-PR cap (#2270): an author-less (ghost) open PR among the repo's others is excluded from the count and the sibling-wake scan, not crashed on", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // A ghost PR with no `user` at all (authorLogin ends up null) — must not match farmer99's count, and must - // not crash the sibling-wake scan, which runs the identical (authorLogin ?? "") fallback. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 50, title: "Ghost PR", state: "open", head: { sha: "ghost50" }, labels: [], body: "z" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 53, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f53" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR two", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if ((url.endsWith("/pulls/53") || url.endsWith("/pulls/54")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/pulls/55") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 55, state: "closed" }); } - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/55/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/55/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-cap-ghost-author", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer's 3rd PR", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - // Ghost PR's null authorLogin never matches "farmer99" — the count is still exactly 3 (farmer99's own). - expect(seen.closed).toBe(true); - }); - - function stubAccountAgeFetch(prNumber: number, createdAt: string, seen: { labels: string[]; closed: boolean }) { - return async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) return Response.json({ login: "newbie", created_at: createdAt }); - if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes(`/pulls/${prNumber}/reviews`)) return Response.json([]); - if (url.includes(`/pulls/${prNumber}/commits`)) return Response.json([]); - if (url.endsWith(`/pulls/${prNumber}`) && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: prNumber, state: "closed" }); } - if (url.endsWith(`/pulls/${prNumber}`)) return Response.json({ number: prNumber, state: "open", user: { login: "newbie" }, head: { sha: `s${prNumber}` }, mergeable_state: "clean" }); - // The other-siblings live-state recheck (#2270 complete-set fix) confirms every counted sibling PR is - // still open before trusting it toward the cap — a generic catch-all covers any of newbie's other - // pre-existing PR numbers without hard-coding specific ones. - if (/\/pulls\/\d+$/.test(url)) return Response.json({ state: "open" }); - if (url.includes(`/commits/s${prNumber}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes(`/commits/s${prNumber}/status`)) return Response.json({ state: "success", statuses: [] }); - if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); - return Response.json({}); - }; - } - - it("account-age throttle (#2561): a below-threshold-age account gets the new-account label AND a tighter effective cap", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // Two pre-existing open PRs from the same new author — a cap of 4 (tightened to 2 for a new account) - // means the 3rd PR is already over the tightened cap, even though it's well under the CONFIGURED cap of 4. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie PR one", state: "open", user: { login: "newbie" }, head: { sha: "s60" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie PR two", state: "open", user: { login: "newbie" }, head: { sha: "s61" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - gateCheckMode: "enabled", reviewCheckMode: "required", - // #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, - }); - 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)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-tighter-cap", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 62, title: "Newbie's 3rd PR", state: "open", user: { login: "newbie" }, head: { sha: "s62" }, labels: [], body: "x", mergeable_state: "clean" }, - }, - }); - - expect(seen.labels).toContain("new-account"); - // The tightened cap (ceil(4/2)=2) is already exceeded by the 3rd PR — closed despite being under the raw cap of 4. - expect(seen.closed).toBe(true); - }); - - it("account-age throttle (#2561): stale cached sibling PRs do not inflate the tightened cap into an auto-close", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 66, title: "Stale newbie PR", state: "open", user: { login: "newbie" }, head: { sha: "s66" }, labels: [], body: "x" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenPrCap: 2, - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) return Response.json({ login: "newbie", created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString() }); - if (url.endsWith("/pulls/66")) return Response.json({ number: 66, state: "closed" }); - if (url.includes("/pulls/67/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/67/reviews")) return Response.json([]); - if (url.includes("/pulls/67/commits")) return Response.json([]); - if (url.endsWith("/pulls/67") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ number: 67, state: "closed" }); } - if (url.endsWith("/pulls/67")) return Response.json({ number: 67, state: "open", user: { login: "newbie" }, head: { sha: "s67" }, mergeable_state: "clean" }); - if (url.includes("/commits/s67/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/s67/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/67/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/67/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - if (url.includes("/issues/67/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-stale-tight-cap", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 67, title: "Newbie's live PR", state: "open", user: { login: "newbie" }, head: { sha: "s67" }, labels: [], body: "x", mergeable_state: "clean" }, - }, - }); - - expect(seen.labels).toContain("new-account"); - expect(seen.closed).toBe(false); - }); - - it("account-age throttle (#2561): an account OLDER than the threshold is unaffected — no label, no cap tightening", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 63, title: "Vet PR one", state: "open", user: { login: "newbie" }, head: { sha: "s63" }, labels: [], body: "x" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 64, title: "Vet PR two", state: "open", user: { login: "newbie" }, head: { sha: "s64" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto", label: "auto" }, - 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)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-unaffected", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 65, title: "Vet's 3rd PR", state: "open", user: { login: "newbie" }, head: { sha: "s65" }, labels: [], body: "x", mergeable_state: "clean" }, - }, - }); - - expect(seen.labels).not.toContain("new-account"); - // The RAW cap (4) is not yet exceeded by a 3rd PR — untouched. - expect(seen.closed).toBe(false); - }); - - it("account-age throttle (#2561): the repo OWNER's own PR is never labeled even on a brand-new account", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - gateCheckMode: "enabled", reviewCheckMode: "required", - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) return Response.json({ login: "JSONbored", created_at: new Date().toISOString() }); - if (url.includes("/pulls/66/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/66/reviews")) return Response.json([]); - if (url.includes("/pulls/66/commits")) return Response.json([]); - if (url.endsWith("/pulls/66")) return Response.json({ number: 66, state: "open", user: { login: "JSONbored" }, head: { sha: "s66" }, mergeable_state: "clean" }); - if (url.includes("/commits/s66/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/s66/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/66/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/66/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/66/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-owner-exempt", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 66, title: "Owner's own PR", state: "open", user: { login: "JSONbored" }, head: { sha: "s66" }, labels: [], body: "x", mergeable_state: "clean" }, - }, - }); - - expect(seen.labels).not.toContain("new-account"); - }); - - it("account-age throttle (#2561): disabled (no threshold configured, the default) never fetches the GitHub user or labels", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto", label: "auto" }, - // accountAgeThresholdDays intentionally omitted — off by default. - }); - const seen = { labels: [] as string[], accountAgeUsersFetched: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // Distinct from the UNRELATED, always-on public-contributor-profile lookup (src/github/public.ts), which - // hits this same bare /users/{login} URL but with NO authorization header (GITHUB_PUBLIC_TOKEN unset in - // this test) — only getGithubUserCreatedAt's account-age-specific call sends a Bearer installation token. - if (url.includes("/users/") && (init?.headers as Record | undefined)?.authorization) { - seen.accountAgeUsersFetched = true; - return Response.json({ login: "newbie", created_at: new Date().toISOString() }); - } - if (url.includes("/users/")) return Response.json({ login: "newbie" }); - if (url.includes("/pulls/67/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/67/reviews")) return Response.json([]); - if (url.includes("/pulls/67/commits")) return Response.json([]); - if (url.endsWith("/pulls/67")) return Response.json({ number: 67, state: "open", user: { login: "newbie" }, head: { sha: "s67" }, mergeable_state: "clean" }); - if (url.includes("/commits/s67/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/s67/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/67/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/67/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/67/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-disabled", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 67, title: "Newbie's PR", state: "open", user: { login: "newbie" }, head: { sha: "s67" }, labels: [], body: "x", mergeable_state: "clean" }, - }, - }); - - expect(seen.accountAgeUsersFetched).toBe(false); - expect(seen.labels).not.toContain("new-account"); - }); - - it("account-age throttle (#2561): a configured newAccountLabel is used instead of the default", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto", review_state_label: "auto" }, - 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)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-custom-label", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 68, title: "Newbie's PR", state: "open", user: { login: "newbie" }, head: { sha: "s68" }, labels: [], body: "x", mergeable_state: "clean" }, - }, - }); - - expect(seen.labels).toContain("custom-new-account-label"); - expect(seen.labels).not.toContain("new-account"); - }); - - it("account-age throttle (#2561): a below-threshold account is NOT labeled when the repo has not opted into label autonomy (regression, gate finding)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - gateCheckMode: "enabled", reviewCheckMode: "required", - // autonomy intentionally omitted — deny-by-default ("observe" for every action class, including "review_state_label"). - 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)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-label-not-autonomous", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 69, title: "Newbie's PR", state: "open", user: { login: "newbie" }, head: { sha: "s69" }, labels: [], body: "x", mergeable_state: "clean" }, - }, - }); - - expect(seen.labels).not.toContain("new-account"); - }); - - function stubIssueAccountAgeFetch(issueNumber: number, createdAt: string, seen: { labels: string[]; closed: boolean }) { - return async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) return Response.json({ login: "newbie", created_at: createdAt }); - if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith(`/issues/${issueNumber}`) && method === "PATCH") { - seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; - return Response.json({ state: "closed" }); - } - if (url.includes(`/issues/${issueNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${issueNumber}/labels`) && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.includes(`/issues/${issueNumber}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: "x" }, { status: 201 }); - return Response.json({}); - }; - } - - it("account-age throttle (#2561 issue path): a below-threshold-age account gets the new-account label AND a tighter effective issue cap", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenIssueCap: 4, - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-tighter-cap", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, - }, - }); - - expect(seen.labels).toContain("new-account"); - expect(seen.closed).toBe(true); - }); - - it("account-age throttle (#2561 issue path): when accountAgeThresholdDays is off, no user lookup runs", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenIssueCap: 4, - }); - let accountAgeUsersFetched = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) { accountAgeUsersFetched = true; return Response.json({ login: "newbie", created_at: new Date().toISOString() }); } - if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") return Response.json({ state: "open" }); - if (url.includes("/issues/62/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-off", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, - }, - }); - - expect(accountAgeUsersFetched).toBe(false); - }); - - it("account-age throttle (#2561 issue path): established account uses the full issue cap (no tightening)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Oldbie issue one", state: "open", user: { login: "oldbie" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Oldbie issue two", state: "open", user: { login: "oldbie" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenIssueCap: 4, - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 730 * 24 * 60 * 60 * 1000).toISOString(), seen)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-established", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Oldbie's 3rd issue", state: "open", user: { login: "oldbie" }, labels: [], body: "x" }, - }, - }); - - expect(seen.labels).not.toContain("new-account"); - expect(seen.closed).toBe(false); - }); - - it("account-age throttle (#2561 issue path): does not label when review_state_label is not auto", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto" }, - contributorOpenIssueCap: 4, - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-label-not-autonomous", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, - }, - }); - - expect(seen.labels).not.toContain("new-account"); - expect(seen.closed).toBe(true); - }); - - it("account-age throttle (#2561 issue path): user lookup failure fail-opens to the full configured cap", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Newbie issue one", state: "open", user: { login: "newbie" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Newbie issue two", state: "open", user: { login: "newbie" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - contributorOpenIssueCap: 4, - accountAgeThresholdDays: 30, - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) return new Response("not found", { status: 404 }); - if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } - if (url.includes("/issues/62/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-lookup-fail-open", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Newbie's 3rd issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - }); - - it("account-age throttle (#2561 issue path): a configured newAccountLabel is used instead of the default", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, - newAccountLabel: "custom-new-account-label", - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", stubIssueAccountAgeFetch(62, new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), seen)); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-custom-label", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Newbie's issue", state: "open", user: { login: "newbie" }, labels: [], body: "x" }, - }, - }); - - expect(seen.labels).toContain("custom-new-account-label"); - expect(seen.labels).not.toContain("new-account"); - }); - - it("account-age throttle (#2561 issue path): the repo OWNER's own issue is never labeled even on a brand-new account", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) return Response.json({ login: "JSONbored", created_at: new Date().toISOString() }); - if (url.includes("/issues/70/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/70/labels") && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-owner-exempt", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 70, title: "Owner's own issue", state: "open", user: { login: "JSONbored" }, labels: [], body: "x" }, - }, - }); - - expect(seen.labels).not.toContain("new-account"); - }); - - it("account-age throttle (#2561 issue path): an ADMIN_GITHUB_LOGINS author is never labeled even on a brand-new account", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - ADMIN_GITHUB_LOGINS: "fleet-admin", - }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) return Response.json({ login: "fleet-admin", created_at: new Date().toISOString() }); - if (url.includes("/issues/71/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/71/labels") && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-admin-exempt", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 71, title: "Admin's issue", state: "open", user: { login: "fleet-admin" }, labels: [], body: "x" }, - }, - }); - - expect(seen.labels).not.toContain("new-account"); - }); - - it("account-age throttle (#2561 issue path): a protected automation bot author is never labeled even on a brand-new account", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", review_state_label: "auto" }, - accountAgeThresholdDays: 30, - }); - const seen = { labels: [] as string[], closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/users/")) return Response.json({ login: "dependabot[bot]", created_at: new Date().toISOString() }); - if (url.includes("/issues/72/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/72/labels") && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "account-age-issue-bot-exempt", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 72, title: "Bot issue", state: "open", user: { login: "dependabot[bot]" }, labels: [], body: "x" }, - }, - }); - - expect(seen.labels).not.toContain("new-account"); - }); - - it("contributor open-PR cap (#2270): out-of-order webhook delivery wakes and self-corrects the missed sibling (regression, gate finding on #2479)", async () => { - // PR56 (the NEWER PR) is delivered BEFORE PR55 exists in the DB — a real possibility under concurrent/ - // retried webhook delivery. At that moment PR56 only sees {54, 56} (2 total, AT the cap of 2, not over), - // so it correctly stays open — but a naive "only ever check myself" implementation would leave it open - // FOREVER, since nothing else ever re-evaluates PR56 again. This pins the fix: once PR55's delivery later - // sees the COMPLETE set {54, 55, 56}, it must wake PR56 (not just decide for itself) so PR56 gets a fresh, - // fully-gated re-evaluation and self-corrects. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR zero", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "w" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - }); - const closedNumbers = new Set(); - 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) => { - if (message.type === "agent-regate-pr") fanned.push(message); - return realSend(message, options); - }) as typeof env.JOBS.send; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - for (const [n, sha] of [[54, "f54"], [55, "f55"], [56, "f56"]] as const) { - if (url.includes(`/pulls/${n}/files`)) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes(`/pulls/${n}/reviews`)) return Response.json([]); - if (url.includes(`/pulls/${n}/commits`)) return Response.json([]); - if (url.endsWith(`/pulls/${n}`) && method === "PATCH") { closedNumbers.add(n); return Response.json({ number: n, state: "closed" }); } - if (url.endsWith(`/pulls/${n}`)) return Response.json({ number: n, state: closedNumbers.has(n) ? "closed" : "open", user: { login: "farmer99" }, head: { sha }, mergeable_state: "clean" }); - if (url.includes(`/commits/${sha}/check-runs`)) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes(`/commits/${sha}/status`)) return Response.json({ state: "success", statuses: [] }); - if (url.includes(`/issues/${n}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${n}/labels`) && method === "POST") return Response.json([]); - if (url.includes(`/issues/${n}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes(`/issues/${n}/comments`)) return Response.json([]); - } - return Response.json({}); - }); - - // PR56 arrives FIRST — PR55 does not exist yet, so PR56 sees only {54, 56}: at the cap, not over. - await processJob(env, { - type: "github-webhook", - deliveryId: "burst-pr56-first", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 56, title: "Farmer PR two (out of order)", state: "open", user: { login: "farmer99" }, head: { sha: "f56" }, labels: [], body: "y", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - expect(closedNumbers.has(56)).toBe(false); // correctly not closed YET — the set looked complete at the time - - // PR55 arrives SECOND — now the complete set {54, 55, 56} is visible. PR55 itself ranks within the cap - // (oldest 2 of 3), so it stays open — but PR56 is now discoverably over-cap and must be woken. - await processJob(env, { - type: "github-webhook", - deliveryId: "burst-pr55-second", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - expect(closedNumbers.has(55)).toBe(false); // PR55 itself is within the cap - expect(fanned.some((job) => job.type === "agent-regate-pr" && job.prNumber === 56)).toBe(true); // sibling woken - - // Drain the woken job — PR56's OWN fresh re-evaluation now sees the complete set and self-corrects. - env.JOBS.send = realSend; - for (const job of fanned) await processJob(env, job); - expect(closedNumbers.has(56)).toBe(true); - }); - - it("contributor open-PR cap (#2270): a re-delivered sibling-wake is coalesced — the second discovery does not re-enqueue", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // Pre-seed the coalescing key for PR56 exactly as wakeOverCapSiblingPullRequests itself would after a - // first, already-successful enqueue — proving the SECOND discovery within the window skips re-enqueueing. - await env.SELFHOST_TRANSIENT_CACHE?.set("contributor-cap-wake:jsonbored/gittensory#56", "1", 60); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR zero", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "w" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 56, title: "Farmer PR two (already over cap)", state: "open", user: { login: "farmer99" }, head: { sha: "f56" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - 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) => { - if (message.type === "agent-regate-pr") fanned.push(message); - return realSend(message, options); - }) as typeof env.JOBS.send; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels")) return Response.json([]); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - // PR55 arrives and independently discovers PR56 is over cap — but the wake was already claimed. - await processJob(env, { - type: "github-webhook", - deliveryId: "wake-coalesce-second", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - - expect(fanned).toEqual([]); // coalesced — no duplicate wake enqueued - }); - - it("contributor open-PR cap (#2270): swallows a failed sibling-wake enqueue and does not claim the coalescing key (regression)", async () => { - // If env.JOBS.send() throws (queue backpressure/outage), the wake must be a best-effort fire-and-forget: - // log and move on WITHOUT claiming the coalescing key, so a later discovery can still retry the enqueue. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 54, title: "Farmer PR zero", state: "open", user: { login: "farmer99" }, head: { sha: "f54" }, labels: [], body: "w" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 56, title: "Farmer PR two (already over cap)", state: "open", user: { login: "farmer99" }, head: { sha: "f56" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "advisory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenPrCap: 2, - }); - env.JOBS.send = (async () => { - throw new Error("queue send boom"); - }) as typeof env.JOBS.send; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/55/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+const ok = true;" }]); - if (url.includes("/pulls/55/reviews")) return Response.json([]); - if (url.includes("/pulls/55/commits")) return Response.json([]); - if (url.endsWith("/pulls/55")) return Response.json({ number: 55, state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, mergeable_state: "clean" }); - if (url.includes("/commits/f55/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/f55/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/55/labels")) return Response.json([]); - if (url.includes("/issues/55/comments")) return Response.json([]); - return Response.json({}); - }); - - // PR55 arrives, discovers PR56 is over cap, and the wake enqueue itself fails — must not throw. - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "wake-enqueue-fails", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Farmer PR one", state: "open", user: { login: "farmer99" }, head: { sha: "f55" }, labels: [], body: "x", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }), - ).resolves.not.toThrow(); - - // The coalescing key was NOT claimed (enqueue failed), so a later discovery can still retry. - expect(await env.SELFHOST_TRANSIENT_CACHE?.get("contributor-cap-wake:jsonbored/gittensory#56")).toBeNull(); - }); - - it("contributor open-ISSUE cap (#2270): a contributor's 3rd open issue (over a cap of 2) is labeled + closed deterministically", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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"); - const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } - if (url.includes("/issues/62/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/62/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/62/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-close", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.labels).toContain("spam-cap"); - expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open issues") && c.includes("limit of 2"))).toBe(true); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - }); - - it("contributor open-ISSUE cap (#2270): bounds the sibling live-check fan-out instead of firing one request per open issue at once (#2766 parity)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // SIBLING_COUNT other open issues from the same author, well beyond the concurrency bound, so an unbounded - // Promise.all would fire every live-state GET at once. The cap is set BELOW the total so the newest issue is - // over the cap and the sibling live-verification path actually runs (it walks the complete sibling set). - const SIBLING_COUNT = 30; - const EXPECTED_LIVE_CHECK_CONCURRENCY = 10; // mirrors CONTRIBUTOR_CAP_LIVE_CHECK_CONCURRENCY in processors.ts - const newIssue = SIBLING_COUNT + 1; - for (let number = 1; number <= SIBLING_COUNT; number += 1) { - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number, title: `Prolific issue ${number}`, state: "open", user: { login: "prolific" }, labels: [], body: "x" }); - } - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenIssueCap: SIBLING_COUNT, - }); - let inFlight = 0; - let maxInFlight = 0; - let closed = false; - const siblingCheckPattern = new RegExp(`/issues/(?:${Array.from({ length: SIBLING_COUNT }, (_, i) => i + 1).join("|")})$`); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (siblingCheckPattern.test(url) && method === "GET") { - inFlight += 1; - maxInFlight = Math.max(maxInFlight, inFlight); - await new Promise((resolve) => setTimeout(resolve, 5)); // force genuine overlap between concurrent checks - inFlight -= 1; - return Response.json({ state: "open" }); - } - if (url.endsWith(`/issues/${newIssue}`) && method === "PATCH") { closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } - if (url.includes(`/issues/${newIssue}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-bounded-concurrency", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: newIssue, title: "Prolific author's newest issue", state: "open", user: { login: "prolific" }, labels: [], body: "x" }, - }, - }); - - expect(closed).toBe(true); // the over-cap issue is closed, confirming the sibling live-check path actually ran - expect(maxInFlight).toBeGreaterThan(1); // genuinely concurrent, not accidentally serial - expect(maxInFlight).toBeLessThanOrEqual(EXPECTED_LIVE_CHECK_CONCURRENCY); - }); - - it("contributor open-ISSUE cap (#2270): a maintainer-named autoCloseExemptLogins entry is exempt from the PER-REPO issue cap too (not just the install-wide cap)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Sentry issue one", state: "open", user: { login: "sentry[bot]" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 61, title: "Sentry issue two", state: "open", user: { login: "sentry[bot]" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - autonomy: { close: "auto", label: "auto" }, - contributorOpenIssueCap: 2, - autoCloseExemptLogins: ["sentry[bot]"], - }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-exempt-login", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Sentry's 3rd issue", state: "open", user: { login: "sentry[bot]" }, labels: [], body: "x" }, - }, - }); - - // Exempt: the 3rd issue is NOT closed for the cap, despite being (numerically) over it. - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBe(0); - }); - - it("REGRESSION (#2479 gate finding): a stale-open DB row for an already-closed sibling does NOT inflate the count and wrongly close a newly opened issue within the real cap", async () => { - // Issue #60 is stored `open` locally but is ACTUALLY closed on GitHub (live GET returns closed) -- e.g. a - // webhook this instance hasn't processed yet, or a manual close elsewhere. Without live-verifying it, the - // stale count would be 3 (60, 61, 62) against a cap of 2, wrongly closing #62. Live-verified, the real count - // is 2 (61, 62), within cap. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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 }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/60") && method === "GET") return Response.json({ state: "closed" }); - if (url.endsWith("/issues/61") && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-stale-closed-sibling", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's issue, within the real cap", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - }); - - it("REGRESSION (#2479 gate finding, second pass): a live-check failure for a counted sibling fails SAFE (excluded from the count) rather than counting it toward an irreversible close", async () => { - // Unlike reconcileLiveDuplicateSiblings' fail-open-to-stored contract (safe there because it only re-ranks a - // non-final duplicate-cluster winner recomputed every delivery), this count directly gates an IRREVERSIBLE - // close. An unreadable live fetch for sibling #60 (404) must NOT let it keep counting toward the cap -- - // otherwise a transient fetch failure stacked on a stale "open" DB row would wrongly close a newly opened - // issue that is actually within the real cap. #60 unverifiable + #61 confirmed open + #62 incoming = 2, - // within the cap of 2, so #62 must NOT close. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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 }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/60") && method === "GET") return new Response("not found", { status: 404 }); - if (url.endsWith("/issues/61") && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-live-check-fails-safe", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's issue, within the real cap", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - }); - - it("a live-verified-open sibling still counts toward the cap and closes the incoming issue when genuinely over", async () => { - // Positive-confirmation path: both siblings live-verify as open, so the real count (60, 61, 62 = 3) against - // a cap of 2 is genuine, and #62 correctly closes. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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"); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/60") && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/61") && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - if (url.includes("/issues/62/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/62/labels") || url.includes("/issues/62/comments")) return Response.json([], { status: 201 }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-live-verified-genuine", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's genuinely 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(true); - }); - - it("falls back to GITHUB_PUBLIC_TOKEN for the sibling live-check when the installation token mint fails", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITHUB_PUBLIC_TOKEN: "public-fallback-token" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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 }); - const seen = { closed: false, sawPublicToken: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return new Response("suspended", { status: 401 }); - if (url.endsWith("/issues/60") && method === "GET") { - seen.sawPublicToken = new Headers(init?.headers).get("authorization")?.includes("public-fallback-token") ?? false; - return Response.json({ state: "closed" }); - } - if (url.endsWith("/issues/61") && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-public-token-fallback", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's issue, within the real cap", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.sawPublicToken).toBe(true); - // #60 was live-verified closed via the public-token fallback, so the real count (61, 62) is within cap. - expect(seen.closed).toBe(false); - }); - - it("contributor open-ISSUE cap (#2270): disabled (no cap configured, the default) never closes an over-threshold contributor's issue", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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" }); - // No contributorOpenIssueCap set — the default, disabled state. - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto", label: "auto" } }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-disabled", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - }); - - it("install-wide contributor open-item cap (#2562): an over-install-cap contributor's issue is caught even with NO per-repo contributorOpenIssueCap configured", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - await upsertRepositoryFromGitHub(env, { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, 123); - await upsertIssueFromGitHub(env, "JSONbored/repo-a", { number: 20, title: "Farmer issue on repo-a", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer issue on repo-b", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - // No contributorOpenIssueCap set — only the install-wide env cap should catch this. - await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" } }); - const seen = { closed: false, labels: [] as string[], comments: [] as string[] }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } - if (url.includes("/issues/62/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/62/labels") && method === "POST") { seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); return Response.json([]); } - if (url.includes("/issues/62/comments") && method === "POST") { seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); return Response.json({ id: 1 }, { status: 201 }); } - // Install-wide live-verify (#2562 gate-review follow-up) re-fetches every OTHER counted sibling before - // trusting it toward the cap -- both of farmer99's other open items must resolve as confirmed-open here. - if (url.endsWith("/repos/JSONbored/repo-a/issues/20")) return Response.json({ number: 20, state: "open" }); - if (url.endsWith("/repos/JSONbored/repo-b/issues/10")) return Response.json({ number: 10, state: "open" }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-issue-cap-close", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 3rd issue install-wide", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(true); - expect(seen.labels).toContain("over-contributor-limit"); - // Install-wide cap counts BOTH open PRs and open issues together (#2562 gate-review follow-up), so the - // close message reports the mixed noun rather than a stale "issues"-only phrasing from the old count-only path. - expect(seen.comments.some((c) => c.includes("@farmer99") && c.includes("3 open pull requests and issues") && c.includes("across every repository it gates"))).toBe(true); - }); - - it("install-wide contributor open-item cap (#2562, #4511): env var unset falls back to the real default (20), so an issue author spread across repos well under it is not closed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); // no GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP -- resolves to the DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP, not "no cap" (#4511) - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - await upsertIssueFromGitHub(env, "JSONbored/repo-a", { number: 20, title: "Farmer issue on repo-a", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer issue on repo-b", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" } }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-issue-cap-off-by-default", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 3rd issue install-wide", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - }); - - it("install-wide contributor open-item cap (#2562): an issue author AT (not over) the install-wide cap is not closed, and falls through to the (unset) per-repo issue cap check safely", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - await upsertIssueFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer issue on repo-b", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - // No contributorOpenIssueCap configured -- exercises the (typeof cap !== "number") early return after the - // install-wide check falls through without matching. - await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: { close: "auto", label: "auto" } }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-issue-cap-at-limit", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 2nd issue, at the install-wide limit", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - }); - - it("install-wide contributor open-item cap (#2562): an over-install-cap issue plans no action (observe-only autonomy) and does not execute a close", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [ - { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - { name: "repo-b", full_name: "JSONbored/repo-b", private: false, owner: { login: "JSONbored" } }, - ], - }); - await upsertIssueFromGitHub(env, "JSONbored/repo-a", { number: 20, title: "Farmer issue on repo-a", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }); - await upsertIssueFromGitHub(env, "JSONbored/repo-b", { number: 10, title: "Farmer issue on repo-b", state: "open", user: { login: "farmer99" }, labels: [], body: "y" }); - // autonomy: {} (no acting classes granted) — the plan builds empty, so `planned.length > 0` is false. - await upsertRepositorySettings(env, { repoFullName: "JSONbored/repo-a", autonomy: {} }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-contributor-issue-cap-observe-only", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 3rd issue install-wide, observe-only", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - }); - - it("install-wide contributor open-item cap (#2562): with BOTH the global cap and the per-repo issue cap configured, an author within the global cap still trips the per-repo cap unchanged", async () => { - // Global cap of 5 is never approached (only 1 open item on repo-b), but the per-repo contributorOpenIssueCap - // of 2 on repo-a IS tripped by this author's 3rd repo-a issue -- proves the two checks are independent and - // the per-repo path still runs (typeof cap !== "number" false branch) after the global check falls through. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "5" }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }], - }); - 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 }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; return Response.json({ state: "closed" }); } - if (url.includes("/issues/62/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/62/labels") && method === "POST") return Response.json([]); - if (url.includes("/issues/62/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "global-and-per-repo-issue-cap-both-configured", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "repo-a", full_name: "JSONbored/repo-a", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 3rd repo-a issue, over the per-repo cap only", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(true); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - }); - - it("contributor open-ISSUE cap (#2270): the repo OWNER's own issue is never closed even over the cap", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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 }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-owner", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Owner's 3rd issue", state: "open", user: { login: "JSONbored" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - }); - - it("contributor open-ISSUE cap (#2270): a contributor's 2nd issue AT (not over) a cap of 2 is not closed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // 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 }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-at-limit", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 2nd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - }); - - it("contributor open-ISSUE cap (#2270): an over-cap issue is not closed when both label and close autonomy are observe-only", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "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" }); - // 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 }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-no-autonomy", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n ?? 0).toBe(0); - }); - - it("contributor open-ISSUE cap (#2270): a slash-free repoFullName is safely planned (repoOwner computation guard) even though the GitHub call itself can never succeed against that name", async () => { - // A real webhook always carries "owner/repo"; this pins the DEFENSIVE repoFullName.includes("/") ? ... : "" - // fallback (mirroring the PR path's own such guard) against a malformed value WITHOUT crashing the cap - // computation. The actual close attempt legitimately errors — splitRepo() (shared by every GitHub-action - // primitive) rejects any repoFullName that isn't "owner/repo" — and that error is caught and audited, not - // thrown into the webhook handler; a successful close against a slash-free name is not physically possible - // via the real GitHub REST API, so asserting an audited error (not a crash) is the correct expectation. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }, 123); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }], - }); - 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 }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/issues/60") || url.endsWith("/issues/61")) return Response.json({ state: "open" }); - return Response.json({}); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-noslash", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "", id: 1, type: "User" } }, - repository: { name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }, - issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }), - ).resolves.not.toThrow(); - - const closeAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = 'agent.action.close' order by created_at desc limit 1").first<{ outcome: string; detail: string }>(); - expect(closeAudit?.outcome).toBe("error"); - expect(closeAudit?.detail).toMatch(/Invalid repository full name/); - }); - - it("contributor open-ISSUE cap (#2270): an author-less (ghost) open issue among the repo's others is excluded from the count, not crashed on", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", issues: "write" }, events: ["issues"] }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // A ghost issue with no `user` at all (authorLogin ends up null) — must not match farmer99's count nor throw. - 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 }); - const seen = { closed: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if ((url.endsWith("/issues/60") || url.endsWith("/issues/61")) && method === "GET") return Response.json({ state: "open" }); - if (url.endsWith("/issues/62") && method === "PATCH") { seen.closed = true; return Response.json({ state: "closed" }); } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "contributor-issue-cap-ghost-author", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 62, title: "Farmer's 3rd issue", state: "open", user: { login: "farmer99" }, labels: [], body: "x" }, - }, - }); - - // Ghost issue's null authorLogin never matches "farmer99" — the count is still exactly 3 (farmer99's own), - // so the cap-of-2 close fires; a broken nullish fallback would either crash or double-count the ghost. - expect(seen.closed).toBe(true); - }); - - // #1092: prReadyForReview rebases a BEHIND-base PR through the agent executor (gated by update_branch autonomy - // + pull_requests:write) before reviewing, then defers — the synchronize on the new head re-runs review. - async function seedBehindRepo(env: Env, over: { autonomy?: Record; agentPaused?: boolean; perms?: Record; noInstall?: boolean } = {}) { - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - if (!over.noInstall) { - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: over.perms ?? { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - } - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: over.autonomy ?? { merge: "auto", update_branch: "auto" }, - agentPaused: over.agentPaused ?? false, - }); - } - - function behindWebhook() { - return { - type: "github-webhook" as const, - deliveryId: "behind-update-branch", - eventName: "pull_request" as const, - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, permissions: { metadata: "read", pull_requests: "write", issues: "write" }, events: ["pull_request"] }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 48, title: "Behind base", state: "open", user: { login: "contributor" }, head: { sha: "behindsha" }, labels: [], body: "x" }, - }, - }; - } - - it("auto-maintain (#1092): a BEHIND-base PR routes update-branch through the executor, then defers review", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedBehindRepo(env); - let updateBranchCalls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/48/update-branch")) { - updateBranchCalls += 1; - return Response.json({ message: "Updating pull request branch." }, { status: 202 }); - } - if (/\/pulls\/48(?:\?|$)/.test(url)) return Response.json({ number: 48, state: "open", head: { sha: "behindsha" }, mergeable_state: "behind" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, behindWebhook()); - - expect(updateBranchCalls).toBe(1); // the rebase was issued before review - const ub = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.update_branch").first<{ outcome: string }>(); - expect(ub?.outcome).toBe("completed"); - // Deferred for the rebase → no gate verdict published on the stale head. - const merge = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.merge").first<{ n: number }>(); - expect(merge?.n).toBe(0); - }); - - it("auto-maintain (#1092): a behind PR is not rebased when the installation lacks pull_requests:write (falls through)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedBehindRepo(env, { noInstall: true }); - // A stored open PR + the recapture-preview job drive reReviewStoredPullRequest directly (no webhook - // installation upsert), so getInstallation(...) is null → installation?.permissions ?? null hits the null arm. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 48, title: "Behind base", state: "open", user: { login: "contributor" }, head: { sha: "behindsha" }, labels: [], body: "x" }); - let updateBranchCalls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/48/update-branch")) { - updateBranchCalls += 1; - return Response.json({}, { status: 202 }); - } - if (/\/pulls\/48(?:\?|$)/.test(url)) return Response.json({ number: 48, mergeable_state: "behind" }); - // CI still running on the (un-rebased) head → prReadyForReview defers at the CI gate, cleanly. - if (url.includes("/commits/behindsha/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI build", status: "in_progress", conclusion: null }] }); - if (url.includes("/commits/behindsha/status")) return Response.json({ state: "pending", statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { type: "recapture-preview", deliveryId: "rp-48", installationId: 123, repoFullName: "JSONbored/gittensory", prNumber: 48, attempt: 1 }); - - expect(updateBranchCalls).toBe(0); // no installation perms → the executor denies the write; the block falls through - }); - - it("recapture-preview (#1158): a clean PR re-review threads previewPollAttempt into the public-surface publish", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { action: "created", installation: { id: 9101, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "preview-repo", full_name: "owner/preview-repo", private: false, owner: { login: "owner" } }, 9101); - await upsertRepositorySettings(env, { repoFullName: "owner/preview-repo", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertPullRequestFromGitHub(env, "owner/preview-repo", { number: 9, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "c9" }, labels: [], body: "x" }); - await upsertPullRequestFile(env, { repoFullName: "owner/preview-repo", pullNumber: 9, path: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, payload: { patch: "@@\n+export const ok = true;" } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/9/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (/\/pulls\/9(?:\?|$)/.test(url)) return Response.json({ number: 9, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "c9" }, labels: [], body: "x" }); - if (url.includes("/commits/c9/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/c9/status")) return Response.json({ state: "success", statuses: [] }); - return new Response("not found", { status: 404 }); - }); - vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); - - // attempt:2 → previewPollAttempt is defined, so the conditional spread at the publish call takes the - // `{ previewPollAttempt }` arm (the recapture-preview poll path; the sweep/webhook callers omit it). - await expect( - processJob(env, { type: "recapture-preview", deliveryId: "rp-9", installationId: 9101, repoFullName: "owner/preview-repo", prNumber: 9, attempt: 2 }), - ).resolves.toBeUndefined(); - }); - - it("recapture-preview (#review-pre-merge-checks): a slop-gated re-review refreshes the PR's files before publishing", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { action: "created", installation: { id: 9102, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "slop-repo", full_name: "owner/slop-repo", private: false, owner: { login: "owner" } }, 9102); - // slopGateMode != "off" ⇒ shouldCollectSlopEvidence(settings) is true ⇒ reReviewStoredPullRequest enters the - // refresh branch (the file-refresh body), so the stored files reflect the PR's current head before publishing. - await upsertRepositorySettings(env, { repoFullName: "owner/slop-repo", checkRunMode: "off", commentMode: "off", publicSurface: "off", slopGateMode: "advisory" }); - await upsertPullRequestFromGitHub(env, "owner/slop-repo", { number: 11, title: "Slop PR", state: "open", user: { login: "contributor" }, head: { sha: "s11" }, labels: [], body: "x" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/11/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (/\/pulls\/11(?:\?|$)/.test(url)) return Response.json({ number: 11, title: "Slop PR", state: "open", user: { login: "contributor" }, head: { sha: "s11" }, labels: [], body: "x" }); - if (url.includes("/commits/s11/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/s11/status")) return Response.json({ state: "success", statuses: [] }); - return new Response("not found", { status: 404 }); - }); - - await expect( - processJob(env, { type: "recapture-preview", deliveryId: "rp-11", installationId: 9102, repoFullName: "owner/slop-repo", prNumber: 11, attempt: 1 }), - ).resolves.toBeUndefined(); - - // refreshPullRequestDetails ran ⇒ a detail-sync-state row was written for this PR (the if-body executed). - const sync = await env.DB.prepare("select status from pull_request_detail_sync_state where repo_full_name = ? and pull_number = ?").bind("owner/slop-repo", 11).first<{ status: string }>(); - expect(sync?.status).toMatch(/^(complete|partial)$/); - }); - - it("auto-maintain (#778): a repo with no acting autonomy takes no agent action", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - requireLinkedIssue: true, - autonomy: { label: "observe" }, // not acting → agent never runs - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/gate123/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "no-autonomy", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 43, title: "No issue", state: "open", user: { login: "contributor" }, head: { sha: "gate123" }, labels: [], body: "No issue link." }, - }, - }); - - const count = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'agent.action.%'").first<{ n: number }>(); - expect(count?.n).toBe(0); - }); - - it("auto-maintain (#778): takes no terminal action when merge/close/approve autonomy is not granted (gate now fails normally for a non-confirmed author)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { review_state_label: "auto", request_changes: "auto" }, - }); - // No confirmed-miner seed → author is unconfirmed; the manifest's linkedIssue:block + no issue fires a - // blocker, so the gate now FAILS the author normally (#gate-nonconfirmed — confirmed status no longer - // neutralizes the verdict). But this repo grants only review_state_label/request_changes autonomy — NOT - // merge/close/approve — so the failing gate yields a request-changes/label action at most, never a terminal action. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/gate123/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "unconfirmed", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 45, title: "No issue", state: "open", user: { login: "stranger" }, head: { sha: "gate123" }, labels: [], body: "No issue link." }, - }, - }); - - // The failing gate is surfaced (request-changes/label), but with no merge/close/approve autonomy granted the - // bot takes NO TERMINAL action — proving terminal actions require their own autonomy grant, independent of the - // gate verdict. (Auto-close on a failing gate is exercised by the #778 close-autonomy tests below.) - const terminal = await env.DB.prepare("select count(*) as n from audit_events where event_type in ('agent.action.merge','agent.action.close','agent.action.approve')").first<{ n: number }>(); - expect(terminal?.n).toBe(0); - }); - - it("auto-maintain (#778): skips a closed PR even on an agent-configured repo", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { label: "auto" }, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); - if (url.includes("/comments")) return Response.json({ id: 1 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "closed-pr", - eventName: "pull_request", - payload: { - action: "closed", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 46, title: "Closed", state: "closed", user: { login: "contributor" }, head: { sha: "gate123" }, labels: [], body: "x" }, - }, - }); - - const count = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'agent.action.%'").first<{ n: number }>(); - expect(count?.n).toBe(0); - }); - - it("auto-maintain (#778): labels a clean passing PR even with no author and no installation record (dry-run)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - // No installation record seeded → installation lookup returns null (label needs only issues:write, exempt). - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { review_state_label: "auto" }, - agentDryRun: true, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/clean123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/clean123/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "no-author-clean", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - // No `user` → authorLogin is absent; default linkedIssue mode is advisory so the verdict is a clean pass. - pull_request: { number: 47, title: "Clean", state: "open", head: { sha: "clean123" }, labels: [], body: "Closes #1" }, - }, - }); - - const labelAudit = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.action.label").first<{ outcome: string; metadata_json: string }>(); - expect(labelAudit?.outcome).toBe("completed"); - expect(JSON.parse(labelAudit?.metadata_json ?? "{}")).toMatchObject({ mode: "dry_run" }); - }); - - it("publishes an enabled gate when bot PR public output is skipped", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - }); - const calls = { gateChecks: 0, comments: 0, minerList: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([]); - } - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gatebot123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/issues/53/comments")) { - calls.comments += 1; - return Response.json([]); - } - if (url.includes("/check-runs") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; - expect(body).toMatchObject({ status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); - expect(body.conclusion).toBeUndefined(); - calls.gateChecks += 1; - return Response.json({ id: 910 }, { status: 201 }); - } - if (url.includes("/check-runs/910") && method === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; - // The bot author is gated normally now (no confirmation gate); linked-issue block + no issue → failure (#gate-nonconfirmed). - expect(body).toMatchObject({ status: "completed", conclusion: "failure", output: { title: "Gittensory Orb Review Agent: No linked issue detected" } }); - calls.gateChecks += 1; - return Response.json({ id: 910 }); - } - return new Response("not found", { status: 404 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-bot-public-skip", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 53, title: "Bot PR", state: "open", user: { login: "automation-bot", type: "Bot" }, head: { sha: "gatebot123" }, labels: [], body: "No issue link." }, - }, - }); - - expect(calls).toEqual({ gateChecks: 2, comments: 0, minerList: 0 }); - const audit = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") - .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#53") - .first<{ detail: string }>(); - expect(audit?.detail).toBe("bot_author"); - }); - - it("evaluates the gate while suppressing public review output for ignored authors", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - }); - const calls = { gateChecks: 0, comments: 0, minerList: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([]); - } - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/ignoredauthor123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/issues/56/comments")) { - if (method !== "GET") calls.comments += 1; - return Response.json([]); - } - if (url.includes("/check-runs") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; - expect(body).toMatchObject({ status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); - expect(body.conclusion).toBeUndefined(); - calls.gateChecks += 1; - return Response.json({ id: 930 }, { status: 201 }); - } - if (url.includes("/check-runs/930") && method === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; - expect(body).toMatchObject({ - status: "completed", - conclusion: "failure", - output: { title: "Gittensory Orb Review Agent: No linked issue detected" }, - }); - calls.gateChecks += 1; - return Response.json({ id: 930 }); - } - return new Response("not found", { status: 404 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { - gate: { linkedIssue: "block" }, - review: { auto_review: { ignore_authors: ["renovate*"] } }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "ignored-author-skip", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 56, title: "Automated dependency update", state: "open", user: { login: "renovate-release" }, head: { sha: "ignoredauthor123" }, labels: [], body: "No issue link." }, - }, - }); - - expect(calls).toEqual({ gateChecks: 2, comments: 1, minerList: 0 }); - const visibilitySkip = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ? and target_key = ?") - .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#56") - .first<{ detail: string; metadata_json: string }>(); - expect(visibilitySkip?.detail).toBe("ignored_author"); - expect(JSON.parse(visibilitySkip?.metadata_json ?? "{}")).toMatchObject({ deliveryId: "ignored-author-skip" }); - }); - - it("audits ignored authors without a skipped check when review checks are disabled", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - reviewCheckMode: "disabled", - linkedIssueGateMode: "off", - }); - const calls = { github: 0, minerList: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") calls.minerList += 1; - if (url.includes("api.github.com")) calls.github += 1; - return new Response("not found", { status: 404 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { - review: { auto_review: { ignore_authors: ["release-please*"] } }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "ignored-author-no-check", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 57, title: "Automated release", state: "open", user: { login: "release-please-bot" }, head: { sha: "ignorednocheck123" }, labels: [], body: "No issue link." }, - }, - }); - - expect(calls).toEqual({ github: 0, minerList: 0 }); - const skipped = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ? and target_key = ?") - .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#57") - .first<{ detail: string; metadata_json: string }>(); - expect(skipped?.detail).toBe("ignored_author"); - expect(JSON.parse(skipped?.metadata_json ?? "{}")).toMatchObject({ deliveryId: "ignored-author-no-check" }); - }); - - it("keeps surface_off precedence over ignored authors when no PR surface is visible", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - reviewCheckMode: "disabled", - linkedIssueGateMode: "off", - }); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { - review: { auto_review: { ignore_authors: ["renovate*"] } }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "surface-off-before-ignored-author", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 58, title: "Automated dependency update", state: "open", user: { login: "renovate-release" }, head: { sha: "surfaceoff123" }, labels: [], body: "No issue link." }, - }, - }); - - const skips = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ? order by created_at") - .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#58") - .all<{ detail: string }>(); - expect(skips.results.map((row) => row.detail)).toEqual(["surface_off"]); - const publicSkip = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") - .bind("github_app.pr_public_surface_skipped", "JSONbored/gittensory#58") - .first<{ detail: string }>(); - expect(publicSkip ?? null).toBeNull(); - }); - - it("publishes an enabled gate when Gittensor-only public output is skipped for an unconfirmed miner", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicAudienceMode: "gittensor_only", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - }); - const calls = { minerList: 0, gateChecks: 0, comments: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([]); - } - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gateminer123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/issues/54/comments")) { - calls.comments += 1; - return Response.json([]); - } - if (url.includes("/check-runs") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; - expect(body).toMatchObject({ status: "in_progress", output: { title: "Gittensory Orb Review Agent is evaluating" } }); - expect(body.conclusion).toBeUndefined(); - calls.gateChecks += 1; - return Response.json({ id: 920 }, { status: 201 }); - } - if (url.includes("/check-runs/920") && method === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; - // The unconfirmed miner is gated normally now; linked-issue block + no issue → failure (#gate-nonconfirmed). - expect(body).toMatchObject({ status: "completed", conclusion: "failure", output: { title: "Gittensory Orb Review Agent: No linked issue detected" } }); - calls.gateChecks += 1; - return Response.json({ id: 920 }); - } - return new Response("not found", { status: 404 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-unconfirmed-miner-public-skip", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 54, title: "Unconfirmed miner PR", state: "open", user: { login: "newbie" }, head: { sha: "gateminer123" }, labels: [], body: "No issue link." }, - }, - }); - - expect(calls).toEqual({ minerList: 1, gateChecks: 2, comments: 0 }); - const audit = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") - .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#54") - .first<{ detail: string }>(); - expect(audit?.detail).toBe("not_official_gittensor_miner"); - }); - - it("keeps gate checks without double-auditing unavailable miner detection as not official", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicAudienceMode: "gittensor_only", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - }); - - const calls = { minerList: 0, gateChecks: 0, comments: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return new Response("gittensor unavailable", { status: 503 }); - } - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gateunavailable123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/issues/55/comments")) { - calls.comments += 1; - return Response.json([]); - } - if (url.includes("/check-runs") && method === "POST") { - calls.gateChecks += 1; - return Response.json({ id: 921 }, { status: 201 }); - } - if (url.includes("/check-runs/921") && method === "PATCH") { - calls.gateChecks += 1; - return Response.json({ id: 921 }); - } - return new Response("not found", { status: 404 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-unavailable-miner-public-skip", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 55, title: "Unavailable miner PR", state: "open", user: { login: "newbie" }, head: { sha: "gateunavailable123" }, labels: [], body: "No issue link." }, - }, - }); - - expect(calls).toEqual({ minerList: 1, gateChecks: 2, comments: 0 }); - const audit = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ? order by id") - .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#55") - .all<{ detail: string }>(); - expect(audit.results.map((event) => event.detail)).toEqual(["miner_detection_unavailable"]); - }); - - it("hard-blocks a confirmed Gittensor contributor in a gate-only configuration when a configured blocker fires", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicAudienceMode: "oss_maintainer", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - }); - const calls = { minerList: 0, gateChecks: 0 }; - let gatePatchBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([ - { uid: 7, githubUsername: "confirmed-dev", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/confirmed123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs/940") && method === "PATCH") { - gatePatchBody = JSON.parse(String(init?.body ?? "{}")) as typeof gatePatchBody; - calls.gateChecks += 1; - return Response.json({ id: 940 }); - } - if (url.includes("/check-runs") && method === "POST") { - calls.gateChecks += 1; - return Response.json({ id: 940 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { linkedIssue: "block" } }); - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-confirmed-block", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 61, title: "Add helper", state: "open", user: { login: "confirmed-dev" }, head: { sha: "confirmed123" }, labels: [], body: "Adds a helper." }, - }, - }); - - // A confirmed contributor with a configured hard blocker (linked-issue gate set to block, no issue - // linked) IS blocked even when the Gate is the only public output, and the Gate names the exact - // blocker so the fix is obvious. - expect(calls.minerList).toBe(1); - expect(calls.gateChecks).toBe(2); - expect(gatePatchBody.conclusion).toBe("failure"); - expect(gatePatchBody.output?.title).toBe("Gittensory Orb Review Agent: No linked issue detected"); - }); - - it("hard-blocks a confirmed contributor on a dual-model AI consensus defect when aiReview: block is opted in", async () => { - const defectJson = JSON.stringify({ - assessment: "Introduces a likely crash.", - blockers: ["Unhandled null dereference on empty input in src/a.ts — the new branch dereferences a possibly-null value."], - nits: ["Guard the null case."], - suggestions: ["Guard the null case."], - }); - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: defectJson }) } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "block", - // Also exercise the opt-in slop advisory in the same surface pass: it persists a per-PR assessment - // and runs the (advisory-only) AI slop pass, but never blocks — the gate still fails on the AI - // consensus defect alone. - slopGateMode: "advisory", - slopAiAdvisory: true, - }); - let gatePatchBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; - const cacheReadSpy = vi - .spyOn(repositoriesModule, "getCachedAiReview") - .mockRejectedValueOnce(new Error("cache read failed")); - const cacheWriteSpy = vi - .spyOn(repositoriesModule, "putCachedAiReview") - .mockRejectedValueOnce(new Error("cache write failed")); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { uid: 7, githubUsername: "confirmed-dev", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/aidefect123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs/950") && method === "PATCH") { - gatePatchBody = JSON.parse(String(init?.body ?? "{}")) as typeof gatePatchBody; - return Response.json({ id: 950 }); - } - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 950 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-ai-consensus-block", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 71, title: "Add helper", state: "open", user: { login: "confirmed-dev" }, head: { sha: "aidefect123" }, labels: [], body: "Adds a helper." }, - }, - }); - - expect(gatePatchBody.conclusion).toBe("failure"); - expect(gatePatchBody.output?.title).toContain("AI reviewers agree on a likely critical defect"); - // The AI usage event was recorded for the review (never with key material). - const usage = await env.DB.prepare("select feature, status from ai_usage_events where feature = ?").bind("ai_review_pr").first<{ feature: string; status: string }>(); - expect(usage).toMatchObject({ feature: "ai_review_pr", status: "ok" }); - expect(cacheReadSpy).toHaveBeenCalled(); - expect(cacheReadSpy.mock.calls[0]?.[5]).toMatch(/^ai-review-input:v4:/); - expect(cacheWriteSpy).toHaveBeenCalled(); - expect(cacheWriteSpy.mock.calls[0]?.[5]).toMatchObject({ - metadata: { inputFingerprint: expect.stringMatching(/^ai-review-input:v4:/) }, - }); - cacheReadSpy.mockRestore(); - cacheWriteSpy.mockRestore(); - }); - - it("finalizes the Gate to neutral instead of leaving it in_progress when gate completion fails", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - }); - const patchBodies: Array<{ status?: string; conclusion?: string; output?: { title?: string } }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/finalize123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 970 }, { status: 201 }); // pending - if (url.includes("/check-runs/970") && method === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; - patchBodies.push(body); - // First PATCH = the gate completion; fail it transiently so the catch must finalize the check. - if (patchBodies.length === 1) return new Response(JSON.stringify({ message: "server error" }), { status: 500 }); - return Response.json({ id: 970 }); - } - return new Response("not found", { status: 404 }); - }); - const realPrepare = env.DB.prepare.bind(env.DB); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - env.DB.prepare = ((sql: string) => { - if (/insert\s+into\s+["`]?check_summaries["`]?/i.test(sql)) throw new Error("summary write failed"); - return realPrepare(sql); - }) as typeof env.DB.prepare; - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-finalize-on-error", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 80, title: "Some change", state: "open", user: { login: "contributor" }, head: { sha: "finalize123" }, labels: [], body: "No issue link." }, - }, - }); - - // The completion PATCH failed (500), so the LOCAL check-run catch finalized the SAME check run (id 970) to - // a neutral, non-blocking terminal state — never left hanging in_progress — and CONTINUED the review - // (no re-throw), so the comment/audit/auto-action still run instead of the whole review dead-lettering. - expect(patchBodies.length).toBe(2); - const finalize = patchBodies[1]; - expect(finalize?.status).toBe("completed"); - expect(finalize?.conclusion).toBe("neutral"); - expect(finalize?.output?.title).toBe("Gittensory Orb Review Agent — could not finish evaluating"); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? and target_key = ?") - .bind("github_app.gate_check_failed_nonfatal", "JSONbored/gittensory#80") - .first<{ outcome: string }>(); - expect(audit?.outcome).toBe("error"); - expect(errors.mock.calls.some((call) => String(call[0]).includes("gate_check_summary_upsert_failed"))).toBe(true); - errors.mockRestore(); - }); - - it("does not stamp a current public surface when a required Gate check never finalizes", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - let commentPosts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-missing/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 972 }, { status: 201 }); - if (url.includes("/check-runs/972") && method === "PATCH") return new Response("check update failed", { status: 500 }); - if (url.includes("/issues/82/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/82/comments") && method === "POST") { - commentPosts += 1; - return Response.json({ id: 8200, html_url: "https://github.com/comment/8200" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-missing-but-comment-posted", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 82, title: "Comment cannot mask missing gate", state: "open", user: { login: "contributor" }, head: { sha: "gate-missing" }, labels: [], body: "No issue link." }, - }, - }); - - expect(commentPosts).toBeGreaterThan(0); - const stored = await getPullRequest(env, "JSONbored/gittensory", 82); - expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); - const incomplete = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_incomplete") - .first<{ detail: string; metadata_json: string }>(); - expect(incomplete?.detail).toBe("required gate check did not finalize"); - expect(incomplete?.metadata_json).toContain('"publishedOutputs":["comment"]'); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_published") - .all(); - expect(published.results).toEqual([]); - const summary = await env.DB.prepare("select id from check_summaries where repo_full_name = ? and pull_number = ? and head_sha = ?") - .bind("JSONbored/gittensory", 82, "gate-missing") - .first<{ id: string }>(); - expect(summary ?? null).toBeNull(); - }); - - it("records the intended label in incomplete-surface audits when a label publishes but Gate never finalizes", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - let labelPosts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-missing-label/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 978 }, { status: 201 }); - if (url.includes("/check-runs/978") && method === "PATCH") return new Response("check update failed", { status: 500 }); - if (url.includes("/issues/88/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/88/labels") && method === "POST") { - labelPosts += 1; - return Response.json([{ name: "gittensor" }]); - } - if (url.includes("/labels") && method === "POST") return Response.json({ name: "gittensor" }, { status: 201 }); - if (url.includes("/labels/") && method === "DELETE") return new Response(null, { status: 204 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-missing-label-published", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 88, title: "Label cannot mask missing gate", state: "open", user: { login: "contributor" }, head: { sha: "gate-missing-label" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(labelPosts).toBeGreaterThan(0); - const stored = await getPullRequest(env, "JSONbored/gittensory", 88); - expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); - const incomplete = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_incomplete") - .first<{ metadata_json: string }>(); - const metadata = JSON.parse(incomplete?.metadata_json ?? "{}"); - expect(metadata).toMatchObject({ - label: "gittensor", - publishedOutputs: ["label"], - }); - }); - - it("does not stamp a gate-only surface when the incomplete-surface audit write fails", async () => { - const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; - let incompleteAuditWrites = 0; - const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { - if (event.eventType === "github_app.pr_public_surface_incomplete") { - incompleteAuditWrites += 1; - throw new Error("audit failed"); - } - await originalRecordAuditEvent(auditEnv, event); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-zero-missing/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 973 }, { status: 201 }); - if (url.includes("/check-runs/973") && method === "PATCH") return new Response("check update failed", { status: 500 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-missing-zero-output", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 83, title: "Gate only missing", state: "open", user: { login: "contributor" }, head: { sha: "gate-zero-missing" }, labels: [], body: "No issue link." }, - }, - }); - - expect(incompleteAuditWrites).toBe(1); - const stored = await getPullRequest(env, "JSONbored/gittensory", 83); - expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); - auditSpy.mockRestore(); - }); - - it("does not stamp a comment surface when the incomplete-surface audit write fails", async () => { - const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; - let incompleteAuditWrites = 0; - const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { - if (event.eventType === "github_app.pr_public_surface_incomplete") { - incompleteAuditWrites += 1; - throw new Error("audit failed"); - } - await originalRecordAuditEvent(auditEnv, event); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate-comment-missing/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 974 }, { status: 201 }); - if (url.includes("/check-runs/974") && method === "PATCH") return new Response("check update failed", { status: 500 }); - if (url.includes("/issues/84/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/84/comments") && method === "POST") return Response.json({ id: 8400, html_url: "https://github.com/comment/8400" }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-missing-comment-audit-fails", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 84, title: "Comment missing gate", state: "open", user: { login: "contributor" }, head: { sha: "gate-comment-missing" }, labels: [], body: "No issue link." }, - }, - }); - - expect(incompleteAuditWrites).toBe(1); - const stored = await getPullRequest(env, "JSONbored/gittensory", 84); - expect(stored?.lastPublishedSurfaceSha ?? null).toBeNull(); - auditSpy.mockRestore(); - }); - - it("propagates a rate-limited Gate completion so the queue retries and the pending Gate stays reviewing", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - }); - const patchBodies: Array<{ status?: string; conclusion?: string; output?: { title?: string } }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/forbidden403/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 971 }, { status: 201 }); // pending in_progress - if (url.includes("/check-runs/971") && method === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; - patchBodies.push(body); - // Gate completion stays rate-limited through the inline retry budget. It must propagate to the queue instead - // of being swallowed as nonfatal; the pending check remains in_progress while the queue backs off and retries. - return new Response(JSON.stringify({ message: "You have exceeded a secondary rate limit" }), { status: 403, headers: { "retry-after": "0" } }); - } - return new Response("not found", { status: 404 }); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "gate-finalize-on-403", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 81, title: "Some change", state: "open", user: { login: "contributor" }, head: { sha: "forbidden403" }, labels: [], body: "No issue link." }, - }, - }), - ).rejects.toThrow(/rate limit/i); - - expect(patchBodies).toHaveLength(4); // initial attempt + GITHUB_RATE_LIMIT_MAX_RETRIES (3) - expect(patchBodies[0]?.status).toBe("completed"); - }); - - it("disables the gate from .gittensory.yml (gate.enabled: false) even when repo settings enable it", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", - requireLinkedIssue: true, - }); - // Config turns the gate OFF even though repo settings have gateCheckMode: enabled. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { enabled: false } }); - const calls = { gateChecks: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs")) { - calls.gateChecks += 1; - return Response.json({ id: 999 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-yml-disabled", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 70, title: "No issue", state: "open", user: { login: "contributor" }, head: { sha: "ymldisabled123" }, labels: [], body: "No issue." }, - }, - }); - - // gate.enabled: false in .gittensory.yml disables the gate entirely — no Gate check is posted. - expect(calls.gateChecks).toBe(0); - }); - - it("audits opt-in gate check permission failures without blocking webhook processing", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - requireLinkedIssue: true, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/gate403/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs")) return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-permission-missing", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 42, title: "Gate without issue", state: "open", user: { login: "contributor" }, head: { sha: "gate403" }, labels: [], body: "No issue link." }, - }, - }); - - const audit = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") - .bind("github_app.gate_check_permission_missing") - .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); - - expect(audit).toMatchObject({ - event_type: "github_app.gate_check_permission_missing", - actor: "contributor", - target_key: "JSONbored/gittensory#42", - outcome: "error", - }); - expect(audit?.detail).toMatch(/Checks: write permission is missing/i); - }); - - it("marks closed PR gates skipped without creating late first comments", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - }); - const calls = { gateWrites: 0, commentGets: 0, commentPosts: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/closed123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string; status?: string; conclusion?: string; output?: { title?: string } }; - expect(body).toMatchObject({ name: "Gittensory Orb Review Agent", status: "completed", conclusion: "skipped", output: { title: "Gittensory Orb Review Agent skipped" } }); - calls.gateWrites += 1; - return Response.json({ id: 901 }, { status: 201 }); - } - if (url.includes("/issues/43/comments") && method === "GET") { - calls.commentGets += 1; - return Response.json([]); - } - if (url.includes("/issues/43/comments") && method === "POST") { - calls.commentPosts += 1; - return Response.json({ id: 1 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-closed", - eventName: "pull_request", - payload: { - action: "closed", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 43, title: "Fast merged PR", state: "closed", user: { login: "contributor" }, head: { sha: "closed123" }, labels: [], body: "Fixes #1" }, - }, - }); - - // The real review is PRESERVED on close: the gate check is marked skipped (gateWrites:1), but the unified - // comment is NOT touched (commentGets:0, commentPosts:0) — no post-close pass overwrites the open-time review - // with an empty skip card. (#preserve-review-on-close) - expect(calls).toEqual({ gateWrites: 1, commentGets: 0, commentPosts: 0 }); - }); - - it("audits closed PR skipped gate permission failures (no late panel write — the real review is preserved)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - }); - let commentGets = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs")) return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); - if (url.includes("/issues/47/comments")) { - commentGets += 1; - return new Response("comments down", { status: 503 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-closed-permission-missing", - eventName: "pull_request", - payload: { - action: "closed", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 47, title: "Fast merged PR", state: "closed", user: { login: "contributor" }, head: { sha: "closed403" }, labels: [], body: "Fixes #1" }, - }, - }); - - // No late panel update on close (the real review is preserved), so the comment endpoint is never hit. - expect(commentGets).toBe(0); - const audit = await env.DB.prepare("select target_key, outcome, detail from audit_events where event_type = ?") - .bind("github_app.gate_check_permission_missing") - .first<{ target_key: string; outcome: string; detail: string }>(); - expect(audit).toMatchObject({ - target_key: "JSONbored/gittensory#47", - outcome: "error", - }); - expect(audit?.detail).toMatch(/Checks: write permission is missing/i); - const webhook = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("gate-closed-permission-missing").first<{ status: string }>(); - expect(webhook?.status).toBe("processed"); - }); - - it("reruns the sticky PR panel when a maintainer checks the rerun task", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicAudienceMode: "oss_maintainer", - publicSignalLevel: "standard", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - includeMaintainerAuthors: true, - commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { "review-now": ["maintainer"] } }, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 45, - title: "Refresh panel", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "panel123" }, - labels: [], - body: "Validation: npm test", - }); - const checkedPanel = [ - "", - "", - "- [x] Re-run Gittensory review", - ].join("\n"); - const calls = { token: 0, permission: 0, minerList: 0, commentGets: 0, commentPatches: 0, checkRuns: 0 }; - let patchedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - // A confirmed official Gittensor contributor → the rerun renders the FULL readiness panel - // (which carries the rerun task); a non-registered author would get the minimal invite. - return Response.json([ - { uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - if (url.includes("/check-runs")) { - calls.checkRuns += 1; - return Response.json({ id: 888 }); - } - if (url.includes("/collaborators/maintainer/permission")) { - calls.permission += 1; - return Response.json({ permission: "maintain" }); - } - if (url.includes("/issues/45/comments") && method === "GET") { - calls.commentGets += 1; - return Response.json([{ id: 777, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); - } - if (url.includes("/issues/comments/777") && method === "PATCH") { - calls.commentPatches += 1; - patchedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 777 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-retrigger", - eventName: "issue_comment", - payload: { - action: "edited", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 45, title: "Refresh panel", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 777, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - // token: 1 — the installation token is now cached + reused within the request (was 2: main + permission check). - // commentGets/commentPatches: 2 — first the purple reviewing placeholder, then the final refreshed panel. - expect(calls).toEqual({ token: 1, permission: 1, minerList: 1, commentGets: 2, commentPatches: 2, checkRuns: 0 }); - expect(patchedBody).toContain(""); - expect(patchedBody).toContain("Readiness score:"); - expect(patchedBody).toContain("- [ ] Re-run Gittensory review"); - expect(patchedBody).not.toContain("- [x] "); - const audit = await env.DB.prepare("select event_type, actor, target_key, outcome from audit_events where event_type = ?") - .bind("github_app.pr_panel_retriggered") - .first<{ event_type: string; actor: string; target_key: string; outcome: string }>(); - expect(audit).toMatchObject({ - event_type: "github_app.pr_panel_retriggered", - actor: "maintainer", - target_key: "JSONbored/gittensory#45", - outcome: "completed", - }); - const usageEvents = await listProductUsageEvents(env, { limit: 5 }); - expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ surface: "github_app", eventName: "pr_panel_retriggered", outcome: "completed" })])); - }); - - it("defers a manual panel rerun while CI is still running", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - includeMaintainerAuthors: true, - autonomy: { merge: "auto" }, - commandAuthorization: { default: ["maintainer"], commands: { "review-now": ["maintainer"] } }, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 46, - title: "Pending CI rerun", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "pendingci" }, - base: { ref: "main" }, - labels: [], - body: "Validation: npm test", - }); - const checkedPanel = [ - "", - "", - "- [x] Re-run Gittensory review", - ].join("\n"); - env.SELFHOST_TRANSIENT_CACHE = { - get: async () => { - throw new Error("Redis unavailable"); - }, - set: async () => undefined, - }; - const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; - const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { - if (event.eventType === "github_app.pr_panel_retrigger_deferred") - throw new Error("D1 audit failed"); - await originalRecordAuditEvent(auditEnv, event); - }); - let commentPatches = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); - if (/\/pulls\/46(?:\?|$)/.test(url)) return Response.json({ number: 46, mergeable_state: "clean" }); - if (url.includes("/commits/pendingci/check-runs")) { - return Response.json({ check_runs: [{ name: "test", status: "in_progress", conclusion: null, app: { slug: "github-actions" } }] }); - } - if (url.includes("/commits/pendingci/status")) return Response.json({ statuses: [] }); - if (url.includes("/issues/comments/778") && method === "PATCH") { - commentPatches += 1; - return Response.json({ id: 778 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-retrigger-ci-pending", - eventName: "issue_comment", - payload: { - action: "edited", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 46, title: "Pending CI rerun", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - expect(commentPatches).toBe(0); - expect(auditSpy).toHaveBeenCalledWith( - env, - expect.objectContaining({ - eventType: "github_app.pr_panel_retrigger_deferred", - actor: "maintainer", - targetKey: "JSONbored/gittensory#46", - outcome: "queued", - }), - ); - auditSpy.mockRestore(); - }); - - it("refreshes the PR's files on a manual rerun so the slop/manifest gate evaluates the current diff", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicAudienceMode: "oss_maintainer", - publicSignalLevel: "standard", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - includeMaintainerAuthors: true, - // Slop gate on → the rerun must refresh the PR files before evaluating (the guard fires). - slopGateMode: "advisory", - commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { "review-now": ["maintainer"] } }, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 45, - title: "Refresh panel", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "panel123" }, - labels: [], - body: "Validation: npm test", - }); - const checkedPanel = ["", "", "- [x] Re-run Gittensory review"].join("\n"); - const calls = { pullsFiles: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([{ uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); - // The refresh fetches files/reviews/checks; count the files fetch to prove the refresh ran on the rerun. - if (url.includes("/pulls/45/files")) { - calls.pullsFiles += 1; - return Response.json([{ filename: "src/app.ts", status: "modified", additions: 5, deletions: 1, changes: 6 }]); - } - if (url.includes("/pulls/45/reviews")) return Response.json([]); - if (url.includes("/commits/panel123/check-runs")) return Response.json({ check_runs: [] }); - if (url.includes("/issues/45/comments") && method === "GET") return Response.json([{ id: 777, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); - if (url.includes("/issues/comments/777") && method === "PATCH") return Response.json({ id: 777 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-retrigger-refresh", - eventName: "issue_comment", - payload: { - action: "edited", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 45, title: "Refresh panel", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 777, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - // The rerun fetched the PR's current files before publishing the panel/gate — not the stale cache. - expect(calls.pullsFiles).toBeGreaterThanOrEqual(1); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? and target_key = ?") - .bind("github_app.pr_panel_retriggered", "JSONbored/gittensory#45") - .first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - }); - - it("skips PR panel reruns from confirmed-miner PR authors because the checkbox is maintainer-only", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - includeMaintainerAuthors: true, - // Even if repo config tries to allow confirmed miners, the checkbox is a maintainer/write-collaborator - // control because it mutates the bot's persisted review comment. - commandAuthorization: { default: ["maintainer", "collaborator", "confirmed_miner"], commands: { "review-now": ["maintainer", "confirmed_miner"] } }, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 48, - title: "Miner self-rerun", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "panel480" }, - labels: [], - body: "Validation: npm test", - }); - const checkedPanel = ["", "", "- [x] Re-run Gittensory review"].join("\n"); - const calls = { minerList: 0, permission: 0, commentPatches: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([{ uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // The confirmed-miner author has NO repo write/admin — authorized via confirmed_miner, not maintainer. - if (url.includes("/collaborators/contributor/permission")) { - calls.permission += 1; - return Response.json({ permission: "none" }); - } - if (url.includes("/issues/48/comments") && method === "GET") return Response.json([{ id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); - if (url.includes("/issues/comments/778") && method === "PATCH") { - calls.commentPatches += 1; - return Response.json({ id: 778 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-retrigger-miner", - eventName: "issue_comment", - payload: { - action: "edited", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 48, title: "Miner self-rerun", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "contributor", type: "User" }, - }, - }); - - // The checkbox authorization ignores the widened repo command policy, so it never reaches miner detection or - // comment mutation for a plain PR author. - expect(calls.minerList).toBe(0); - expect(calls.permission).toBe(1); - expect(calls.commentPatches).toBe(0); - const audit = await env.DB.prepare("select actor, outcome, detail from audit_events where event_type = ? and target_key = ?") - .bind("github_app.pr_panel_retrigger_skipped", "JSONbored/gittensory#48") - .first<{ actor: string; outcome: string; detail: string }>(); - expect(audit).toMatchObject({ actor: "contributor", outcome: "completed", detail: "maintainer_command_requires_maintainer" }); - }); - - it("skips PR panel reruns from users without repository write permission", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 46, - title: "Unauthorized panel refresh", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "panel-denied" }, - labels: [], - body: "Validation: npm test", - }); - const checkedPanel = [ - "", - "", - "- [x] Re-run Gittensory review", - ].join("\n"); - const calls = { token: 0, permission: 0, commentGets: 0, commentPatches: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - if (url.includes("/collaborators/drive-by-user/permission")) { - calls.permission += 1; - return Response.json({ permission: "read" }); - } - if (url.includes("/issues/46/comments")) { - calls.commentGets += 1; - return Response.json([{ id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); - } - if (url.includes("/issues/comments/778")) { - calls.commentPatches += 1; - return Response.json({ id: 778 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-retrigger-denied", - eventName: "issue_comment", - payload: { - action: "edited", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 46, title: "Unauthorized panel refresh", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 778, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "drive-by-user", type: "User" }, - }, - }); - - expect(calls).toEqual({ token: 1, permission: 1, commentGets: 0, commentPatches: 0 }); - const audit = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") - .bind("github_app.pr_panel_retrigger_skipped") - .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); - expect(audit).toMatchObject({ - event_type: "github_app.pr_panel_retrigger_skipped", - actor: "drive-by-user", - target_key: "JSONbored/gittensory#46", - outcome: "completed", - detail: "not_maintainer_or_pr_author", - }); - }); - - it("reruns the sticky PR panel when a write collaborator checks the rerun task", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicAudienceMode: "oss_maintainer", - publicSignalLevel: "standard", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - includeMaintainerAuthors: true, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 47, - title: "Refresh panel as collaborator", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "panel-writer" }, - labels: [], - body: "Validation: npm test", - }); - const checkedPanel = [ - "", - "", - "- [x] Re-run Gittensory review", - ].join("\n"); - const calls = { token: 0, permission: 0, minerList: 0, commentGets: 0, commentPatches: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([]); - } - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - if (url.includes("/collaborators/writer/permission")) { - calls.permission += 1; - return Response.json({ permission: "write" }); - } - if (url.includes("/issues/47/comments") && method === "GET") { - calls.commentGets += 1; - return Response.json([{ id: 779, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }]); - } - if (url.includes("/issues/comments/779") && method === "PATCH") { - calls.commentPatches += 1; - return Response.json({ id: 779 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-retrigger-writer", - eventName: "issue_comment", - payload: { - action: "edited", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 47, title: "Refresh panel as collaborator", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 779, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "writer", type: "User" }, - }, - }); - - // token: 1 — the installation token is now cached + reused within the request (was 2: main + permission check). - // commentGets/commentPatches: 2 — first the purple reviewing placeholder, then the final refreshed panel. - expect(calls).toEqual({ token: 1, permission: 1, minerList: 1, commentGets: 2, commentPatches: 2 }); - }); - - it("skips PR panel reruns when the editing actor and PR author are unavailable", async () => { - const env = createTestEnv(); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 48, - title: "Unknown panel refresh actor", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "panel-unknown" }, - labels: [], - body: "Validation: npm test", - }); - await env.DB.prepare("update pull_requests set author_login = null where repo_full_name = ? and number = ?").bind("JSONbored/gittensory", 48).run(); - const checkedPanel = [ - "", - "", - "- [x] Re-run Gittensory review", - ].join("\n"); - vi.stubGlobal("fetch", async () => new Response("unexpected fetch", { status: 500 })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-retrigger-unknown-actor", - eventName: "issue_comment", - payload: { - action: "edited", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 48, title: "Unknown panel refresh actor", state: "open", pull_request: {} }, - comment: { id: 780, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - }, - }); - - const audit = await env.DB.prepare("select actor, target_key, detail from audit_events where event_type = ?") - .bind("github_app.pr_panel_retrigger_skipped") - .first<{ actor: string | null; target_key: string; detail: string }>(); - expect(audit).toMatchObject({ - actor: null, - target_key: "JSONbored/gittensory#48", - detail: "not_maintainer_or_pr_author", - }); - }); - - it("ignores invalid rerun task edits and audits skipped rerun requests", async () => { - const env = createTestEnv(); - const checkedPanel = [ - "", - "", - "- [x] Re-run Gittensory review", - ].join("\n"); - const uncheckedPanel = checkedPanel.replace("- [x]", "- [ ]"); - let fetchCalls = 0; - vi.stubGlobal("fetch", async () => { - fetchCalls += 1; - return new Response("unexpected fetch", { status: 500 }); - }); - const basePayload = { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 46, title: "Panel skip", state: "open", user: { login: "contributor" }, pull_request: {} }, - }; - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", {}); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-created-ignore", - eventName: "issue_comment", - payload: { - action: "created", - ...basePayload, - comment: { id: 800, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-unchecked-ignore", - eventName: "issue_comment", - payload: { - action: "edited", - ...basePayload, - comment: { id: 801, body: uncheckedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-non-bot-ignore", - eventName: "issue_comment", - payload: { - action: "edited", - ...basePayload, - comment: { id: 802, body: checkedPanel, user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-missing-comment-ignore", - eventName: "issue_comment", - payload: { action: "edited", ...basePayload, sender: { login: "maintainer", type: "User" } }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-missing-panel-marker-ignore", - eventName: "issue_comment", - payload: { - action: "edited", - ...basePayload, - comment: { id: 806, body: "- [x] Re-run Gittensory review", user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-missing-rerun-marker-ignore", - eventName: "issue_comment", - payload: { - action: "edited", - ...basePayload, - comment: { id: 807, body: "\n\n- [x] Re-run Gittensory review", user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-other-bot-ignore", - eventName: "issue_comment", - payload: { - action: "edited", - ...basePayload, - comment: { id: 808, body: checkedPanel, user: { login: "other[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-bot-skip", - eventName: "issue_comment", - payload: { - action: "edited", - ...basePayload, - comment: { id: 803, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "gittensory[bot]", type: "Bot" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-missing-cache", - eventName: "issue_comment", - payload: { - action: "edited", - ...basePayload, - comment: { id: 804, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "panel-rerun-missing-context", - eventName: "issue_comment", - payload: { - action: "edited", - comment: { id: 805, body: checkedPanel, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - expect(fetchCalls).toBe(0); - const skips = await env.DB.prepare("select detail from audit_events where event_type = ? order by detail") - .bind("github_app.pr_panel_retrigger_skipped") - .all<{ detail: string }>(); - expect(skips.results.map((event) => event.detail)).toEqual(["bot_author", "cached_pr_missing", "missing_repo_pr_or_installation"]); - }); - - it("debounces noisy PR events without publishing public surfaces", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_and_label", - autoLabelEnabled: true, - checkRunMode: "enabled", - gateCheckMode: "enabled", reviewCheckMode: "required", - }); - let publicCalls = 0; - vi.stubGlobal("fetch", async () => { - publicCalls += 1; - return new Response("unexpected public call", { status: 500 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", {}); - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-labeled-noisy", - eventName: "pull_request", - payload: { - action: "labeled", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 44, title: "Noisy event PR", state: "open", user: { login: "contributor" }, head: { sha: "noisy123" }, labels: [{ name: "bug" }], body: "Fixes #1" }, - }, - }); - - expect(publicCalls).toBe(0); - }); - - it("processes GitHub webhook jobs for PRs, issues, comments-off, comment-attempt, and deleted installs", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 1, - title: "Prior merged work", - state: "closed", - merged_at: "2026-05-01T00:00:00.000Z", - user: { login: "oktofeesh1" }, - labels: [{ name: "bug" }], - body: "Fixes #1", - }); - const visibleCalls = { comments: 0, labelsCreated: 0, labelsApplied: 0, checks: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; - expect(body.body).toContain(""); - expect(body.body).toContain("Confirmed Gittensor contributor"); - expect(body.body).not.toMatch(/reviewability|likely_duplicate|reward|scoreability|estimated score|wallet|hotkey|trust score|payout|farming/i); - visibleCalls.comments += 1; - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - if (url.includes("/issues/3/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/labels") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { labels?: string[] }; - expect(body.labels).toEqual(["gittensor"]); - visibleCalls.labelsApplied += 1; - return Response.json([{ name: "gittensor" }]); - } - if (url.includes("/repos/JSONbored/gittensory/labels") && !url.includes("/issues/") && method === "GET") return Response.json([]); - if (url.includes("/repos/JSONbored/gittensory/labels") && !url.includes("/issues/") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string }; - expect(body.name).toBe("gittensor"); - visibleCalls.labelsCreated += 1; - return Response.json({ name: "gittensor" }, { status: 201 }); - } - if (url.includes("/check-runs")) { - visibleCalls.checks += 1; - return new Response("checks disabled", { status: 500 }); - } - return new Response("not found", { status: 404 }); - }); - - const basePayload = { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - }; - - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSignalLevel: "standard", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - backfillEnabled: true, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-off", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - pull_request: { - number: 2, - title: "Fix webhook duplicate delivery", - state: "open", - user: { login: "oktofeesh1" }, - labels: [{ name: "bug" }], - body: "Fixes #1", - }, - }, - }); - expect(await listPullRequests(env, "JSONbored/gittensory")).toEqual(expect.arrayContaining([expect.objectContaining({ number: 2 })])); - - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: true, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - backfillEnabled: true, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-comment-attempt", - eventName: "pull_request", - payload: { - action: "synchronize", - ...basePayload, - pull_request: { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - labels: [{ name: "bug" }], - body: "Fixes #1", - }, - }, - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-comment-undetected", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - pull_request: { - number: 4, - title: "New contributor work", - state: "open", - user: { login: "newbie" }, - labels: [], - body: "Fixes #1", - }, - }, - }); - - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "minimal", - publicSurface: "comment_and_label", - autoLabelEnabled: true, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - backfillEnabled: true, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-comment-no-author", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - pull_request: { - number: 5, - title: "Anonymous webhook work", - state: "open", - labels: [], - body: "Fixes #1", - }, - }, - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "issue", - eventName: "issues", - payload: { - action: "opened", - ...basePayload, - issue: { - number: 1, - title: "Webhook duplicate delivery", - state: "open", - user: { login: "reporter" }, - labels: [{ name: "bug" }], - body: "Duplicate delivery should be idempotent.", - }, - }, - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "deleted", - eventName: "installation", - payload: { action: "deleted", installation: { id: 123 } }, - }); - - expect(visibleCalls).toEqual({ comments: 1, labelsCreated: 1, labelsApplied: 1, checks: 0 }); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ? order by created_at").bind("github_app.pr_visibility_skipped").all<{ - detail: string; - }>(); - expect(skipped.results.map((event) => event.detail)).toEqual(expect.arrayContaining(["not_official_gittensor_miner", "missing_author"])); - }); - - // #1007 convergence (Stage D): with GITTENSORY_REVIEW_UNIFIED_COMMENT on AND the gate evaluating, the public PR-panel - // comment is rendered by the UNIFIED renderer (GitHub alert + synthesized "Code review" row) instead of the - // legacy panel — while STILL leading with the same panel marker so the in-place upsert updates the same - // comment. Mirrors the legacy panel-posting setup (confirmed miner + comment_and_label) but flips the flag - // and enables the gate so `maybePublishPrPublicSurface` takes the flag-ON branch. - it("renders the unified PR-review comment when the flag is on and the gate evaluates", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - }); - let postedBody = ""; - const calls = { comments: 0, gateChecks: 0 }; - let gateFinalized = false; - let failedPostGateMint = false; - const liveCiSpy = vi - .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") - .mockRejectedValueOnce(new Error("transient CI read failed")) - .mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) { - if (gateFinalized && !failedPostGateMint) { - failedPostGateMint = true; - return new Response("mint failed", { status: 500 }); - } - return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - } - // PR files — the unified branch (re)fetches them to count changed files for the readiness chip. - if (url.includes("/pulls/3/files")) return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); - // #review-audit: the LIVE merge-state the comment now reads — the base just advanced with a conflict, so the - // live state is `dirty` even though the stored mergeableState (unset on this payload) would not say so. - if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "dirty" }); - // Gate check-run — must succeed so `gateEvaluation` is produced and the flag-ON branch runs. - // The pending check is POSTed (in_progress), then PATCHed to its completed conclusion. - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - calls.gateChecks += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; - if (body.status !== "in_progress" || body.conclusion) { - gateFinalized = true; - clearInstallationTokenCacheForTest(); - } - return Response.json({ id: 901 }, { status: 201 }); - } - if (url.includes("/check-runs/901") && method === "PATCH") { - calls.gateChecks += 1; - gateFinalized = true; - clearInstallationTokenCacheForTest(); - return Response.json({ id: 901 }); - } - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - calls.comments += 1; - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-unified-comment", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "unified123" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - const installationTokenCiReads = liveCiSpy.mock.calls.filter( - ([, , , token]) => token === "installation-token", - ); - expect(installationTokenCiReads).toHaveLength(2); - expect(calls.comments).toBe(2); - expect(failedPostGateMint).toBe(true); - // Still leads with the panel marker → the upsert updates the SAME sticky comment in place (no duplicate). - expect(postedBody).toContain(""); - // The UNIFIED shape, which the legacy body never emits: a full-comment GitHub alert wrapper… - expect(postedBody).toMatch(/> \[!(TIP|NOTE|WARNING|CAUTION)\]/); - // …and the renderer's synthesized "Code review" signal row (bold first table label). - expect(postedBody).toContain("**Code review**"); - // Public-safe by construction — no internal trust/economics fields leak through the unified renderer. - expect(postedBody).not.toMatch(/wallet|hotkey|reward|trust score/i); - // #review-audit (#4220): the comment reads the LIVE `dirty` merge-state (not the stale stored one), so it must - // NOT headline "safe to merge" while the disposition would auto-close the base-conflicting PR. - expect(postedBody).not.toMatch(/safe to merge/i); - // #1955: no `.gittensory.yml` was fetched here (the raw-content URL isn't stubbed, so it 404s and the - // manifest resolves to null) — review.effort_score is absent/default OFF, so the effort chip must NOT render. - expect(postedBody).not.toMatch(/review effort:/); - } finally { - liveCiSpy.mockRestore(); - } - }); - - it("INVARIANT (#4498): the disposition planner reuses the public surface's own live mergeable_state/CI read instead of re-fetching a third time", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - }); - let mergeableStateReads = 0; - // No mockRejectedValueOnce here -- unlike the "renders the unified PR-review comment" test above, every call - // succeeds identically, isolating the "both refreshes succeed" case this fix targets (a prior-call failure - // legitimately forces a genuine second live read, which is a different, already-covered scenario). - const liveCiSpy = vi.spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl").mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - // commentMode: "detected_contributors_only" requires the author to actually resolve as a detected - // Gittensor contributor for the unified-comment (and its live merge-state/CI refresh) code path to - // engage at all -- an empty miner match here would silently skip that whole block, same as the - // original "renders the unified PR-review comment" test's fixture this one is adapted from. - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { uid: 7, githubUsername: "oktofeesh1", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1, hotkey: "must-not-leak" }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - if (url.includes("/pulls/3/files")) return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); - if (/\/pulls\/3(?:\?|$)/.test(url) && method === "GET") { - mergeableStateReads += 1; - return Response.json({ number: 3, mergeable_state: "clean" }); - } - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-single-live-fetch", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Single live fetch per pass", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "singlefetch123" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - // 2, not 3: readiness's own cachedLiveMergeState/cachedLiveCiAggregate check contributes ONE legitimate, - // unrelated live read each (a genuine durable-cache miss on this never-before-seen head, unaffected by - // this fix), and maybePublishPrPublicSurface's own forced refresh contributes the other -- reused - // directly by the disposition planner instead of re-fetched a third time. Verified empirically: reverting - // this fix on this exact fixture produces 3 of each, confirming the fix removes exactly the redundant - // third call, not readiness's separate, necessary one. - expect(mergeableStateReads).toBe(2); - const installationTokenCiReads = liveCiSpy.mock.calls.filter(([, , , token]) => token === "installation-token"); - expect(installationTokenCiReads).toHaveLength(2); - } finally { - liveCiSpy.mockRestore(); - } - }); - - // #3609/#3610: same fixture as the unified-comment test above (screenshotsAllowed needs both the global flag - // AND the repo cutover allowlist — createTestEnv already defaults GITTENSORY_REVIEW_REPOS to include this - // repo), but the changed file is WEB-VISIBLE (isVisualPath) so the capture pipeline actually fires, proving - // resolveVisualCaptureConfig / buildCapture's config-threading (review.visual) is reached end to end from the - // real webhook path, not just from the pure-function unit tests in visual-capture.test.ts. - it("threads review.visual config into the capture pipeline and renders a Visual preview section (#3609 / #3610)", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", - GITTENSORY_REVIEW_SCREENSHOTS: "true", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - }); - let postedBody = ""; - const liveCiSpy = vi.spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl").mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) { - return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - } - // A web-visible route file (isVisualPath) — the ONLY difference from the sibling unified-comment fixture — - // so screenshotsAllowed's file-touch gate opens and buildCapture actually runs for this PR. - if (url.includes("/pulls/3/files")) { - return Response.json([{ filename: "apps/gittensory-ui/src/routes/app.index.tsx", additions: 5, deletions: 1, status: "modified" }]); - } - if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - // Preview discovery (deployments / commit checks / PR comments): none configured for this fixture, so - // buildCapture's discovery chain finds nothing and falls back to placeholders — it's wrapped in its own - // try/catch, so a 404 here degrades to "no preview" rather than failing the capture or the review. - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-visual-config-wiring", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Update the app index route", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "visualcfg123" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - // The capture pipeline ran (resolveVisualCaptureConfig -> buildCapture, both reached only through this - // webhook path) and produced at least a placeholder-backed route, so the collapsible renders. - expect(postedBody).toContain("Visual preview"); - expect(postedBody).toContain("`/app`"); - // Public-safe by construction — no internal trust/economics fields leak through the shot URLs either. - expect(postedBody).not.toMatch(/wallet|hotkey|reward|trust score/i); - } finally { - liveCiSpy.mockRestore(); - } - }); - - // #4083: review.visual.enabled: false (config-as-code, VPS-only in practice) overrides the coarser - // GITTENSORY_REVIEW_SCREENSHOTS + GITTENSORY_REVIEW_REPOS env-var gate above — same fixture as the sibling - // test above (same webhook, same visual-file touch, same env flag ON), the ONLY difference being the - // .gittensory.yml content, so this isolates the new enabled:false branch in processors.ts. - it("skips the capture pipeline entirely when review.visual.enabled is false, even though the env-var gate allows it (#4083)", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", - GITTENSORY_REVIEW_SCREENSHOTS: "true", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - }); - let postedBody = ""; - const liveCiSpy = vi.spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl").mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n visual:\n enabled: false\n"); - } - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) { - return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - } - if (url.includes("/pulls/3/files")) { - return Response.json([{ filename: "apps/gittensory-ui/src/routes/app.index.tsx", additions: 5, deletions: 1, status: "modified" }]); - } - if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 901 }, { status: 201 }); - if (url.includes("/check-runs/901") && method === "PATCH") return Response.json({ id: 901 }); - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-visual-config-disabled", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Update the app index route", - state: "open", - user: { login: "oktofeesh1" }, - // Empty sha + a present ref (the opposite combination from the sibling "threads review.visual config" - // test's { sha: "visualcfg123" }) so between the two tests, both branches of captureTarget's - // optional headSha/headRef spreads are exercised. - head: { sha: "", ref: "feature/visual-config-disabled" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - // review.visual.enabled: false overrode the env-var gate — no capture attempted, so no Visual preview - // section at all, even though the PR touches a visual file and GITTENSORY_REVIEW_SCREENSHOTS is on. - expect(postedBody).not.toContain("Visual preview"); - } finally { - liveCiSpy.mockRestore(); - } - }); - - // #1957: with the unified comment on AND `.gittensory.yml` opting into `review.changed_files_summary`, the - // rendered comment gains the deterministic "Changed files" collapsible built from the SAME PR-files fetch the - // unified branch already does for the readiness chip — no separate call, no AI. Mirrors the base unified-comment - // test above but adds the manifest opt-in and asserts the new section's presence + content. - it("renders the Changed files summary when review.changed_files_summary is on in .gittensory.yml", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - }); - let postedBody = ""; - const calls = { comments: 0, gateChecks: 0 }; - let gateFinalized = false; - let failedPostGateMint = false; - const liveCiSpy = vi - .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") - .mockRejectedValueOnce(new Error("transient CI read failed")) - .mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - // .gittensory.yml opts into the deterministic changed-files summary — no AI involved. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n changed_files_summary: true\n"); - } - if (url.includes("/access_tokens")) { - if (gateFinalized && !failedPostGateMint) { - failedPostGateMint = true; - return new Response("mint failed", { status: 500 }); - } - return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - } - // PR files — the unified branch (re)fetches them to count changed files AND (with the toggle above) to - // build the "Changed files" summary. A doc + a source file so the summary shows 2 distinct category rows. - if (url.includes("/pulls/3/files")) - return Response.json([ - { filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }, - { filename: "README.md", additions: 2, deletions: 0, status: "modified" }, - ]); - if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); - // Gate check-run — must succeed so `gateEvaluation` is produced and the flag-ON branch runs. - // The pending check is POSTed (in_progress), then PATCHed to its completed conclusion. - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - calls.gateChecks += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; - if (body.status !== "in_progress" || body.conclusion) { - gateFinalized = true; - clearInstallationTokenCacheForTest(); - } - return Response.json({ id: 901 }, { status: 201 }); - } - if (url.includes("/check-runs/901") && method === "PATCH") { - calls.gateChecks += 1; - gateFinalized = true; - clearInstallationTokenCacheForTest(); - return Response.json({ id: 901 }); - } - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - calls.comments += 1; - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-unified-comment-changed-files", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "unified456" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - expect(calls.comments).toBe(2); - expect(postedBody).toContain(""); - // The deterministic changed-files collapsible — per-file rows with GitHub Files-tab links (#2157). - expect(postedBody).toContain("Changed files"); - expect(postedBody).toContain("| `src/cache.ts` | +5 | -1 | [View diff](https://github.com/JSONbored/gittensory/pull/3/files#diff-"); - expect(postedBody).toContain("| `README.md` | +2 | -0 | [View diff](https://github.com/JSONbored/gittensory/pull/3/files#diff-"); - } finally { - liveCiSpy.mockRestore(); - } - }); - - // #1955: with the unified comment on AND `.gittensory.yml` opting into `review.effort_score`, the rendered - // comment gains the deterministic, no-AI "review effort: N/5 (~M min)" chip — computed by estimateReviewEffort - // from the SAME PR-files fetch the unified branch already does (no separate call). Mirrors the - // changed_files_summary test above but asserts the effort chip's presence + exact value instead. - it("renders the review effort chip when review.effort_score is on in .gittensory.yml", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - }); - let postedBody = ""; - const calls = { comments: 0, gateChecks: 0 }; - let gateFinalized = false; - let failedPostGateMint = false; - const liveCiSpy = vi - .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") - .mockRejectedValueOnce(new Error("transient CI read failed")) - .mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - // .gittensory.yml opts into the deterministic effort score — no AI involved. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n effort_score: true\n"); - } - if (url.includes("/access_tokens")) { - if (gateFinalized && !failedPostGateMint) { - failedPostGateMint = true; - return new Response("mint failed", { status: 500 }); - } - return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - } - // PR files — the unified branch (re)fetches them to count changed files AND (with the toggle above) to - // compute the effort estimate. A 10-added-line source file WITH a patch (weighted 10) plus a docs file with - // NO `patch` field (exercises the `typeof file.payload?.patch === "string" ? ... : undefined` fallback -> - // addedLineCount(undefined) = 0, so it contributes 0 weighted lines but still its per-file overhead): - // weighted 10 + 0 + 2 files * 3 overhead = effort 16 -> band 2, minutes round(16 * 0.5) = 8 - // (see estimateReviewEffort — src/review/review-effort.ts). - if (url.includes("/pulls/3/files")) - return Response.json([ - { - filename: "src/cache.ts", - additions: 10, - deletions: 1, - status: "modified", - patch: `@@ -1,1 +1,11 @@\n${Array.from({ length: 10 }, (_, i) => `+const x${i} = ${i};`).join("\n")}`, - }, - { filename: "README.md", additions: 2, deletions: 0, status: "modified" }, - ]); - if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); - // Gate check-run — must succeed so `gateEvaluation` is produced and the flag-ON branch runs. - // The pending check is POSTed (in_progress), then PATCHed to its completed conclusion. - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - calls.gateChecks += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; - if (body.status !== "in_progress" || body.conclusion) { - gateFinalized = true; - clearInstallationTokenCacheForTest(); - } - return Response.json({ id: 901 }, { status: 201 }); - } - if (url.includes("/check-runs/901") && method === "PATCH") { - calls.gateChecks += 1; - gateFinalized = true; - clearInstallationTokenCacheForTest(); - return Response.json({ id: 901 }); - } - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - calls.comments += 1; - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-unified-comment-effort-score", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "unified789" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - expect(calls.comments).toBe(2); - expect(postedBody).toContain(""); - // The new deterministic, no-AI chip: band 2 (effort 16 <= BAND_MAX[1]=40), minutes round(16*0.5)=8. - expect(postedBody).toContain("`review effort: 2/5 (~8 min)`"); - } finally { - liveCiSpy.mockRestore(); - } - }); - - // #2051/#4147: with the unified comment on AND `.gittensory.yml` opting into `review.auto_merge_summary`, - // the rendered comment gains the deterministic, no-AI "Auto-merge readiness" collapsible — computed from the - // SAME live CI state, gate conclusion, mergeable_state, and linked-issue facts this pass already resolves - // for the readiness chip and gate verdict, no extra fetch. Mirrors the effort_score test above but asserts - // the auto-merge-readiness table's presence + condition marks instead. - it("renders the Auto-merge readiness collapsible when review.auto_merge_summary is on in .gittensory.yml", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - }); - let postedBody = ""; - const calls = { comments: 0, gateChecks: 0 }; - let gateFinalized = false; - let failedPostGateMint = false; - const liveCiSpy = vi - .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") - .mockRejectedValueOnce(new Error("transient CI read failed")) - .mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - // .gittensory.yml opts into the deterministic auto-merge summary — no AI involved. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n auto_merge_summary: true\n"); - } - if (url.includes("/access_tokens")) { - if (gateFinalized && !failedPostGateMint) { - failedPostGateMint = true; - return new Response("mint failed", { status: 500 }); - } - return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - } - if (url.includes("/pulls/3/files")) - return Response.json([{ filename: "src/cache.ts", additions: 10, deletions: 1, status: "modified" }]); - // mergeable_state: "clean" -> mergeableClean: true in the rendered table. - if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); - // Gate check-run — must succeed so `gateEvaluation` concludes "success" -> gatePassing: true. - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - calls.gateChecks += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; - if (body.status !== "in_progress" || body.conclusion) { - gateFinalized = true; - clearInstallationTokenCacheForTest(); - } - return Response.json({ id: 901 }, { status: 201 }); - } - if (url.includes("/check-runs/901") && method === "PATCH") { - calls.gateChecks += 1; - gateFinalized = true; - clearInstallationTokenCacheForTest(); - return Response.json({ id: 901 }); - } - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - calls.comments += 1; - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-unified-comment-auto-merge-summary", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "unified789" }, - labels: [{ name: "bug" }], - // A linked issue (#1) is present -> linkedIssueValid: true in the rendered table. - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - expect(calls.comments).toBe(2); - expect(postedBody).toContain(""); - expect(postedBody).toContain("Auto-merge readiness"); - expect(postedBody).toContain("_Read-only snapshot of the current auto-merge conditions"); - // All four conditions pass with this fixture: CI green, gate passing, branch mergeable clean, valid - // linked issue. - expect(postedBody).toContain("| CI checks green | ✅ |"); - expect(postedBody).toContain("| Gate passing | ✅ |"); - expect(postedBody).toContain("| Branch mergeable (clean) | ✅ |"); - expect(postedBody).toContain("| Valid linked issue | ✅ |"); - } finally { - liveCiSpy.mockRestore(); - } - }); - - // #2044: `.gittensory.yml` `review.tone` is folded into the AI reviewer's system prompt by - // composeManifestReviewInstructions (src/signals/focus-manifest.ts), consumed by - // src/queue/processors.ts's aiReviewCacheReadDecideAndRun. That composition is unit-tested in isolation - // (focus-manifest.test.ts), but nothing previously drove the full webhook -> processJob -> runGittensoryAiReview - // pipeline to confirm the resolved tone text actually reaches env.AI.run's system message. Mirrors the - // changed_files_summary/effort_score tests above but captures the AI system prompt instead of the posted body. - it("threads review.tone from .gittensory.yml into the AI reviewer's system prompt (#2044)", async () => { - let capturedSystem = ""; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async (_model: string, options: { messages: Array<{ role: string; content: string }> }) => { - capturedSystem = options.messages[0]?.content ?? ""; - return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; - }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - // The repo's own review.tone opt-in (#2044) -- a maintainer voice brief, distinct from review.instructions. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n tone: Keep findings terse and skip pleasantries\n"); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-tone-system-prompt", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, - }, - }); - - // The composed tone section (composeManifestReviewInstructions) really reached env.AI.run's system message -- - // not just the pure-function assertion in focus-manifest.test.ts. - expect(capturedSystem).toContain( - "Review tone (maintainer voice brief — complements review.profile): Keep findings terse and skip pleasantries", - ); - }); - - // #review-exclude-paths / #2043: `review.exclude_paths`/`review.path_filters` are resolved by - // resolveReviewPromptOverrides and applied by filterReviewFilesForAi (src/signals/focus-manifest.ts), consumed - // by src/queue/processors.ts's runAiReviewForAdvisory -- but ONLY in advisory mode (block mode intentionally - // reviews the full diff so a filtered path can never bypass an AI consensus blocker). filterReviewFilesForAi - // itself is unit-tested as a pure function (focus-manifest.test.ts); every existing e2e assertion of this field - // elsewhere in this file only ever passes EMPTY excludePaths/pathFilters arrays (cache-fingerprint checks), so - // nothing previously proved a NON-EMPTY glob genuinely removes a matching file from what the AI reviewer sees. - it("genuinely removes a review.exclude_paths match from the AI reviewer's diff in advisory mode (#review-exclude-paths)", async () => { - let capturedUser = ""; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { - run: async (_model: string, options: { messages: Array<{ role: string; content: string }> }) => { - capturedUser = options.messages[1]?.content ?? ""; - return { response: JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: [], suggestions: [] }) }; - }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - // advisory (NOT block): block mode always reviews the full diff, ignoring exclude_paths/path_filters, so - // only advisory mode exercises the filterReviewFilesForAi branch (src/queue/processors.ts). - aiReviewMode: "advisory", - // The PR author below is an unconfirmed contributor; aiReviewAllAuthors is the documented per-repo opt-in - // that widens the AI-spend gate to every author (already unit-tested in ai-review-advisory.test.ts) so this - // test doesn't also have to stand up the full miner-confirmation registry mocks just to reach the AI call. - aiReviewAllAuthors: true, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) - return Response.json([ - { filename: "src/real-change.ts", status: "modified", additions: 3, deletions: 0, patch: "@@ -1,1 +1,4 @@\n+export const real = 1;\n+export const two = 2;\n+export const three = 3;" }, - { filename: "src/schema.generated.ts", status: "modified", additions: 1, deletions: 0, patch: "@@ -1,1 +1,2 @@\n+export const generatedMarker = true;" }, - ]); - if (url.endsWith("/pulls/7")) return Response.json({ number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a7/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a7/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - // The repo's own review.exclude_paths opt-in -- a NON-EMPTY glob (#review-exclude-paths), unlike every - // existing fingerprint-only assertion of this field elsewhere in this file. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response('review:\n exclude_paths:\n - "**/*.generated.ts"\n'); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-exclude-paths-ai-diff", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 7, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "Closes #1" }, - }, - }); - - // The non-excluded file's diff genuinely reached the AI reviewer's user prompt... - expect(capturedUser).toContain("src/real-change.ts"); - // ...but the exclude_paths match is genuinely ABSENT -- not merely uncounted -- from what the AI reviewer - // sees: neither its path nor its patch content leaked into the prompt. - expect(capturedUser).not.toContain("schema.generated.ts"); - expect(capturedUser).not.toContain("generatedMarker"); - }); - - // #2049: with the unified comment on AND `.gittensory.yml` setting `review.max_findings`, the processor wires - // manifest caps into `buildUnifiedCommentBody` and the renderer truncates blocker/nit lists with a "+N more" - // footer. Mirrors the effort_score test above but asserts display-only truncation instead. - it("truncates unified-comment blockers when review.max_findings is set in .gittensory.yml (#2049)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - linkedIssueGateMode: "block", - }); - let postedBody = ""; - const calls = { comments: 0, gateChecks: 0 }; - let gateFinalized = false; - let failedPostGateMint = false; - const liveCiSpy = vi - .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") - .mockRejectedValueOnce(new Error("transient CI read failed")) - .mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n max_findings:\n blockers: 0\n"); - } - if (url.includes("/access_tokens")) { - if (gateFinalized && !failedPostGateMint) { - failedPostGateMint = true; - return new Response("mint failed", { status: 500 }); - } - return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - } - if (url.includes("/pulls/3/files")) - return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); - if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - calls.gateChecks += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; - if (body.status !== "in_progress" || body.conclusion) { - gateFinalized = true; - clearInstallationTokenCacheForTest(); - } - return Response.json({ id: 901 }, { status: 201 }); - } - if (url.includes("/check-runs/901") && method === "PATCH") { - calls.gateChecks += 1; - gateFinalized = true; - clearInstallationTokenCacheForTest(); - return Response.json({ id: 901 }); - } - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - calls.comments += 1; - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-unified-comment-max-findings", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "unifiedmaxfindings" }, - labels: [{ name: "bug" }], - body: "No linked issue on purpose.\n\nValidation: npm test", - }, - }, - }); - - expect(calls.comments).toBe(2); - expect(postedBody).toContain(""); - expect(postedBody).toContain("_+1 more_"); - } finally { - liveCiSpy.mockRestore(); - } - }); - - // #2181 (apply slice of #1964): review.memory end-to-end through the real webhook path. A `qualityGateMode: - // "advisory"` + an unreachable `qualityGateMinScore: 100` deterministically produces the - // `readiness_score_below_threshold` ADVISORY (never a blocker — readiness stays advisory-only, see - // rules.test.ts) warning finding on every pass, giving a stable target to record a suppression signal against - // and verify it is (or is not) suppressed from the rendered unified comment. The manifest is seeded DIRECTLY - // via upsertRepoFocusManifest (bypassing the 6h .gittensory.yml fetch cache) so each test's `.gittensory.yml` - // fetch response is never actually needed on the hot path — it only serves as an inert 404 fallback. - async function runReadinessWarningPass(env: Env, opts: { deliveryId: string; headSha: string; reviewMemoryManifest: boolean }) { - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - autonomy: { update_branch: "auto" }, - qualityGateMode: "advisory", - qualityGateMinScore: 100, - }); - await upsertRepoFocusManifest(env, "JSONbored/gittensory", opts.reviewMemoryManifest ? { review: { memory: true } } : {}); - let postedBody = ""; - let gateFinalized = false; - let failedPostGateMint = false; - const liveCiSpy = vi - .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") - .mockResolvedValue({ - ciState: "passed", - hasPending: false, - hasVisiblePending: false, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("not found", { status: 404 }); - } - if (url.includes("/access_tokens")) { - if (gateFinalized && !failedPostGateMint) { - failedPostGateMint = true; - return new Response("mint failed", { status: 500 }); - } - return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); - } - if (url.includes("/pulls/3/files")) - return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); - if (/\/pulls\/3(?:\?|$)/.test(url)) return Response.json({ number: 3, mergeable_state: "clean" }); - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; - if (body.status !== "in_progress" || body.conclusion) { - gateFinalized = true; - clearInstallationTokenCacheForTest(); - } - return Response.json({ id: 901 }, { status: 201 }); - } - if (url.includes("/check-runs/901") && method === "PATCH") { - gateFinalized = true; - clearInstallationTokenCacheForTest(); - return Response.json({ id: 901 }); - } - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - try { - await processJob(env, { - type: "github-webhook", - deliveryId: opts.deliveryId, - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: opts.headSha }, - labels: [{ name: "bug" }], - // No linked issue AND no validation evidence -- keeps the readiness score comfortably below the - // unreachable qualityGateMinScore: 100 threshold above, so readiness_score_below_threshold fires - // deterministically regardless of the panel's exact scoring breakdown. - body: "No linked issue, no validation evidence on purpose.", - }, - }, - }); - } finally { - liveCiSpy.mockRestore(); - } - return postedBody; - } - - it("FLAG-OFF (default): review.memory in .gittensory.yml alone never suppresses the readiness warning (operator kill-switch required)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - // review.memory: true in the manifest, but NO GITTENSORY_REVIEW_MEMORY env flag on this env -- byte-identical. - const postedBody = await runReadinessWarningPass(env, { - deliveryId: "review-memory-flag-off", - headSha: "revmem-flag-off", - reviewMemoryManifest: true, - }); - expect(postedBody).toContain("Readiness score is below the configured threshold"); - }); - - it("FLAG-ON: suppresses a readiness warning EXACTLY matching a previously recorded suppression signal", async () => { - const seedEnv = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - // A throwaway pass (flag/manifest both off — byte-identical review path) against a SEPARATE, disposable D1 - // instance just to learn the finding's REAL, LIVE-computed readiness score (a pure function of the fixed - // PR/settings fixture above, so it reproduces identically for the real pass below on its own fresh `env`). - // The rendered nit itself only carries `title`+`action` (see buildDualReviewNotes's gateNits) — the score - // comes from the status chip. - const seedBody = await runReadinessWarningPass(seedEnv, { deliveryId: "review-memory-seed", headSha: "revmem-seed", reviewMemoryManifest: false }); - expect(seedBody).toContain("Readiness score is below the configured threshold"); - const scoreMatch = /readiness (\d+)\/100/.exec(seedBody); - expect(scoreMatch).not.toBeNull(); - const score = Number(scoreMatch![1]); - // Reconstructs buildQualityGateWarning's exact title+detail template (src/rules/advisory.ts) from the live - // score + the qualityGateMinScore: 100 configured above, so the computed patternHash matches the real finding. - const detail = `The public readiness score is ${score}/100, below the repository threshold of 100/100.`; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", GITTENSORY_REVIEW_MEMORY: "true" }); - await recordReviewSuppression(env, { - repoFullName: "JSONbored/gittensory", - category: "readiness_score_below_threshold", - patternHash: reviewMemoryFingerprint({ - category: "readiness_score_below_threshold", - message: `Readiness score is below the configured threshold ${detail}`, - }), - createdBy: "maintainer1", - }); - // The flag is ON (env + manifest) and the exact-match signal is now stored -- the warning must be - // suppressed from the rendered unified comment. - const postedBody = await runReadinessWarningPass(env, { - deliveryId: "review-memory-flag-on", - headSha: "revmem-flag-on", - reviewMemoryManifest: true, - }); - expect(postedBody).not.toContain("Readiness score is below the configured threshold"); - }); - - it("FLAG-ON, no stored signals: neither suppresses nor demotes -- the warning renders exactly as if review.memory were off (REGRESSION: the all-clear branch where the store read succeeds but finds nothing to apply)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", GITTENSORY_REVIEW_MEMORY: "true" }); - // Flag is fully ON (env + manifest) and the suppression-store read succeeds, but NO signal has ever been - // recorded for this repo -- applyReviewMemorySuppression's own empty-signals short-circuit returns - // suppressedCount: 0, demotedCount: 0, so processors.ts's "anything to apply?" check is false and - // renderedGate is never reassigned away from the original commentGate. - const postedBody = await runReadinessWarningPass(env, { - deliveryId: "review-memory-no-signals", - headSha: "revmem-no-signals", - reviewMemoryManifest: true, - }); - expect(postedBody).toContain("Readiness score is below the configured threshold"); - }); - - it("FLAG-ON: DEMOTES (keeps, but does not suppress) a same-category readiness warning that does not exactly match any stored signal", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", GITTENSORY_REVIEW_MEMORY: "true" }); - // A signal for the SAME category but a patternHash that can never match this PR's real finding -- exercises - // the "demote" (scope-matched, hash-mismatched) branch instead of "suppress". - await recordReviewSuppression(env, { - repoFullName: "JSONbored/gittensory", - category: "readiness_score_below_threshold", - patternHash: "never-matches-the-real-finding", - createdBy: "maintainer1", - }); - const postedBody = await runReadinessWarningPass(env, { - deliveryId: "review-memory-demote", - headSha: "revmem-demote", - reviewMemoryManifest: true, - }); - // Demoted (not suppressed) -- the finding still renders in the comment. - expect(postedBody).toContain("Readiness score is below the configured threshold"); - }); - - it("FLAG-ON, fail-safe: a suppression-store read error leaves the readiness warning untouched rather than throwing", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", GITTENSORY_REVIEW_MEMORY: "true" }); - const listSpy = vi.spyOn(repositoriesModule, "listReviewSuppressions").mockRejectedValue(new Error("D1 unavailable")); - try { - const postedBody = await runReadinessWarningPass(env, { - deliveryId: "review-memory-store-error", - headSha: "revmem-store-error", - reviewMemoryManifest: true, - }); - expect(postedBody).toContain("Readiness score is below the configured threshold"); - } finally { - listSpy.mockRestore(); - } - }); - - // #1955: the review-effort minutes persisted onto the public-stats audit event (independent of - // review.effort_score, which only gates the unified-comment CHIP) must never block the publish itself when the - // estimator throws — the publish still completes and simply omits `reviewEffortMinutes` from the event metadata - // (public-stats.ts's own COALESCE-style fallback then applies, same as a pre-#1955 historical row). - it("swallows an estimateReviewEffort failure when persisting the public-stats minutes — the publish still completes", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "off", gatePack: "oss-anti-slop" }); - const estimateSpy = vi.spyOn(reviewEffortModule, "estimateReviewEffort").mockImplementationOnce(() => { - throw new Error("estimator blew up"); - }); - let commentPosted = false; - let publishedMetadata: Record | undefined; - const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; - const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { - if (event.eventType === "github_app.pr_public_surface_published") { - publishedMetadata = event.metadata as Record; - } - await originalRecordAuditEvent(auditEnv, event); - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/8/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/8")) return Response.json({ number: 8, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }); - if (url.includes("/commits/a8/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a8/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/8/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/8/comments") && method === "POST") { commentPosted = true; return Response.json({ id: 1 }, { status: 201 }); } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - try { - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "effort-estimator-throws", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 8, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }, - }, - }), - ).resolves.toBeUndefined(); - - expect(commentPosted).toBe(true); // the publish completed despite the estimator throwing - expect(estimateSpy).toHaveBeenCalled(); - expect(publishedMetadata).toBeDefined(); - expect(publishedMetadata).not.toHaveProperty("reviewEffortMinutes"); - } finally { - estimateSpy.mockRestore(); - auditSpy.mockRestore(); - } - }); - - // #1958: with inline comments AND finding categories both on in .gittensory.yml (finding_categories rides on - // inline_comments, exactly like suggestions did for #1956), the model is asked to self-categorize each - // inlineFindings item, and BOTH surfaces render it — the posted inline review comment label AND the unified - // comment's new "Finding categories" collapsible. - it("renders finding categories in the inline comment label and the unified comment's Finding categories section when review.finding_categories is on", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", - GITTENSORY_REVIEW_INLINE_COMMENTS: "true", - GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", - AI: { - run: async () => - ({ - response: JSON.stringify({ - assessment: "Looks fine overall.", - blockers: [], - nits: [], - suggestions: [], - inlineFindings: [ - { path: "src/db.ts", line: 2, severity: "nit", body: "This query is vulnerable to SQL injection.", category: "security" }, - ], - }), - }) as { response: string }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - aiReviewMode: "block", - gatePack: "oss-anti-slop", - }); - let inlineReviewComments: Array<{ body: string }> = []; - let unifiedCommentBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // .gittensory.yml opts into inline comments AND finding categories together. - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - return new Response("review:\n inline_comments: true\n finding_categories: true\n"); - } - if (url.includes("/pulls/8/files")) - return Response.json([{ filename: "src/db.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@ -1,1 +1,2 @@\n ctx\n+export const ok = true;" }]); - if (url.endsWith("/pulls/8")) return Response.json({ number: 8, title: "Add query helper", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a8/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a8/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - // The separate, quiet inline-review post (event: COMMENT) — distinct from the sticky unified issue comment. - if (url.endsWith("/pulls/8/reviews") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { comments?: Array<{ body: string }> }; - inlineReviewComments = body.comments ?? []; - return Response.json({ id: 55 }); - } - if (url.includes("/issues/8/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/8/comments") && method === "POST") { - unifiedCommentBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1 }, { status: 201 }); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-finding-categories", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 8, title: "Add query helper", state: "open", user: { login: "contributor" }, head: { sha: "a8" }, labels: [], body: "Closes #1" }, - }, - }); - - // The inline PR-review comment label carries the category tag. - expect(inlineReviewComments[0]?.body).toBe("**Nit · Security:** This query is vulnerable to SQL injection."); - // The unified comment's new collapsible counts it too. - expect(unifiedCommentBody).toContain("Finding categories"); - expect(unifiedCommentBody).toContain("| Security | 1 |"); - }); - - // #1971: a FROZEN (manual-review) PR reuses its last published AI review, which carries no impact-map entries — - // the unified comment still renders, and the impact-map render arm degrades to no section (aiReview present but - // aiReview.impactMap undefined ⇒ `aiReview?.impactMap ?? []` ⇒ [] ⇒ buildImpactMapCollapsible null). - it("renders the unified comment WITHOUT an Impact map section when a frozen review is reused (no threaded entries)", async () => { - let aiCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", - AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Fresh.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await persistRegistrySnapshot(env, normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z")); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "block", gatePack: "oss-anti-slop" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 77, title: "Held PR", state: "open", user: { login: "contributor" }, head: { sha: "a77" }, labels: [{ name: "manual-review" }], body: "Closes #1" }); - await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 77, status: "complete", reviewsSyncedAt: new Date().toISOString() }); - // A prior PUBLISHED review for this exact head — the freeze path reuses it (aiReview = frozenReview) instead of - // spending a fresh AI call. Its cached shape has notes+reviewerCount but NO impactMap, so the render arm's - // nullish arm fires. - await putCachedAiReview(env, "JSONbored/gittensory", 77, "a77", "block", { notes: "Prior published review.", reviewerCount: 1 }); - await markAiReviewPublished(env, "JSONbored/gittensory", 77, "a77"); - let unifiedCommentBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/pulls/77/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.endsWith("/pulls/77")) return Response.json({ number: 77, title: "Held PR", state: "open", user: { login: "contributor" }, head: { sha: "a77" }, labels: [{ name: "manual-review" }], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a77/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a77/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/issues/77/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/77/comments")) { unifiedCommentBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? unifiedCommentBody); return Response.json({ id: 1 }, { status: 201 }); } - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "impact-map-frozen-reuse", repoFullName: "JSONbored/gittensory", prNumber: 77, installationId: 123 }); - - expect(aiCalls).toBe(0); // frozen ⇒ reused, no fresh AI - expect(unifiedCommentBody).toContain("gittensory-pr-panel"); // the unified panel rendered from the frozen review - expect(unifiedCommentBody).not.toContain("Impact map"); // ...with no impact-map section (reused review has none) - }); - - // #1962: with BOTH the operator flag and the manifest opt-in on, the review emits a "Fix handoff" collapsible — - // one machine-readable block per inline finding a contributor's own local agent can consume — in the unified - // comment. Flag-OFF (every other review test) ⇒ no such section. - it("emits the Fix handoff collapsible in the unified comment when review.fixHandoff + the operator flag are on", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", - GITTENSORY_REVIEW_INLINE_COMMENTS: "true", - GITTENSORY_REVIEW_FIX_HANDOFF: "true", - GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", - AI: { - run: async () => - ({ - response: JSON.stringify({ - assessment: "One real issue.", - blockers: [], - nits: [], - suggestions: [], - inlineFindings: [ - { path: "src/db.ts", line: 2, severity: "blocker", body: "This query is vulnerable to SQL injection.", suggestion: "Use a parameterized query." }, - ], - }), - }) as { response: string }, - } as unknown as Ai, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - AI_DAILY_NEURON_BUDGET: "100000", - }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commentMode: "all_prs", publicSurface: "comment_only", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "block", gatePack: "oss-anti-slop" }); - let unifiedCommentBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") { - // NOTE: the manifest key is camelCase `fixHandoff` (unlike snake-case `finding_categories`) — see focus-manifest parse. - return new Response("review:\n inline_comments: true\n fixHandoff: true\n"); - } - if (url.includes("/pulls/9/files")) - return Response.json([{ filename: "src/db.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@ -1,1 +1,2 @@\n ctx\n+export const ok = true;" }]); - if (url.endsWith("/pulls/9")) return Response.json({ number: 9, title: "Add query helper", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); - if (url.includes("/commits/a9/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a9/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - if (url.endsWith("/pulls/9/reviews") && method === "POST") return Response.json({ id: 55 }); - if (url.includes("/issues/9/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/9/comments") && method === "POST") { - unifiedCommentBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1 }, { status: 201 }); - } - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-fix-handoff", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 9, title: "Add query helper", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "Closes #1" }, - }, - }); - - expect(unifiedCommentBody).toContain("Fix handoff"); // the collapsible section is emitted - expect(unifiedCommentBody).toContain("Fix handoff — Blocker at `src/db.ts:2`"); // the per-finding block header + location anchor - expect(unifiedCommentBody).toContain("This query is vulnerable to SQL injection."); // the finding, handed off verbatim - expect(unifiedCommentBody).toContain("Suggested change:"); // its suggestion carried through - }); - - // FIX B + FIX D3 at the processor call site: a unified comment for a PR whose CI has a FAILED check, with the - // PR's files only available from GitHub (stored rows empty) — proves (B) the inline file fetch populates the - // real diff/changed-file count on the first review, and (D3) the failing check name + its per-check WHY render - // under a "CI checks failing" section (not just a bare "CI failing" chip). - it("inline-fetches the PR files and renders failing CI check names + reasons in the unified comment (FIX B + D3)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITHUB_PUBLIC_TOKEN: "public-token", GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - }); - // Seed a FAILED check summary with a per-check WHY (codecov-style) so listCheckSummaries returns it and the - // unified site populates failingDetails. (The PR row + headSha must match for the check to associate.) - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "unified123" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }); - await upsertCheckSummary(env, { - id: "JSONbored/gittensory#unified123#codecov/patch", - repoFullName: "JSONbored/gittensory", - pullNumber: 3, - headSha: "unified123", - name: "codecov/patch", - status: "completed", - conclusion: "failure", - detailsUrl: "https://codecov.io/report", - payload: { output: { summary: "60% of diff hit (target 97%)" } }, - }); - let postedBody = ""; - let filesFetched = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // FIX B: stored pull_request_files is empty, so the review path inline-fetches from GitHub here. - if (url.includes("/pulls/3/files")) { - filesFetched += 1; - return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified", patch: "@@\n+const x = 1;" }]); - } - // The review path now reads the LIVE CI aggregate (check-runs + commit-statuses). codecov/patch is a - // classic COMMIT-STATUS (not a check-run), so it comes from the combined-status endpoint; the check-runs - // list stays empty (it must, so the gate's own check-run upsert finds no pre-existing run to PATCH). - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/") && url.includes("/status")) - return Response.json({ state: "failure", statuses: [{ context: "codecov/patch", state: "failure", description: "60% of diff hit (target 97%)", target_url: "https://codecov.io/report" }] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 902 }, { status: 201 }); - if (url.includes("/check-runs/902") && method === "PATCH") return Response.json({ id: 902 }); - if (url.includes("/issues/3/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/3/comments") && method === "POST") { - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-unified-ci-failing", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 3, - title: "Fix webhook duplicate delivery again", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "unified123" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - // FIX B: the files were fetched inline from GitHub (stored rows were empty) and the changed-file count is real. - expect(filesFetched).toBeGreaterThan(0); - expect(postedBody).toContain("`1 file`"); - // FIX D3: the failing check name + its WHY render under a "CI checks failing" section, plus the chip. - expect(postedBody).toContain("`CI failing`"); - expect(postedBody).toContain("CI checks failing"); - expect(postedBody).toContain("codecov/patch"); - expect(postedBody).toContain("60% of diff hit (target 97%)"); - // Still public-safe. - expect(postedBody).not.toMatch(/wallet|hotkey|reward|trust score/i); - }); - - // REGRESSION (#4414-class advisory holds): a third-party app's COMPLETED action_required check-run that is - // NOT a branch-protection required context (e.g. Superagent's "Contributor trust", posted alongside its own - // separate, actually-required "Superagent Security Scan") must never flip ciState to "failed" or post under - // "CI checks failing" -- that auto-closes real contributor PRs (#4414's regression). It must still be VISIBLE, - // under its own non-blocking "Flagged checks" section, so a maintainer can act on it without the PR being - // silently waved through OR silently closed. - it("REGRESSION (#4414-class advisory holds): a non-required third-party action_required check renders as a non-blocking 'Flagged checks' note, not a CI failure", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITHUB_PUBLIC_TOKEN: "public-token", GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 6, - title: "Fix flaky retry test", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "flagged456" }, - base: { ref: "main" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { - uid: 7, - githubUsername: "oktofeesh1", - githubId: "123", - totalPrs: 4, - totalMergedPrs: 3, - totalOpenPrs: 1, - totalClosedPrs: 0, - totalOpenIssues: 0, - totalClosedIssues: 0, - totalSolvedIssues: 0, - totalValidSolvedIssues: 0, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - hotkey: "must-not-leak", - }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ - repositories: [ - { - repositoryFullName: "JSONbored/gittensory", - totalPrs: "4", - totalMergedPrs: "3", - totalOpenPrs: "1", - totalClosedPrs: "0", - totalOpenIssues: "0", - totalClosedIssues: "0", - isEligible: true, - credibility: "1.000000", - }, - ], - }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/6/files")) return Response.json([{ filename: "src/retry.ts", additions: 3, deletions: 1, status: "modified", patch: "@@\n+const x = 1;" }]); - // Branch protection requires ONLY "validate" + "Superagent Security Scan" -- NOT "Contributor trust", - // matching the real-world JSONbored/gittensory config that #4414 broke. - if (url.includes("/branches/main/protection/required_status_checks")) return Response.json({ contexts: ["validate", "Superagent Security Scan"] }); - if (url.includes("/check-runs") && method === "GET") { - return Response.json({ - total_count: 3, - check_runs: [ - { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "Superagent Security Scan", status: "completed", conclusion: "success", app: { slug: "superagent-security" } }, - { - name: "Contributor trust", - status: "completed", - conclusion: "action_required", - app: { slug: "superagent-security" }, - output: { title: "Manual review needed" }, - details_url: "https://superagent.example/checks/contributor-trust", - }, - ], - }); - } - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 903 }, { status: 201 }); - if (url.includes("/check-runs/903") && method === "PATCH") return Response.json({ id: 903 }); - if (url.includes("/issues/6/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/6/comments") && method === "POST") { - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-flagged-nonrequired-check", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 6, - title: "Fix flaky retry test", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "flagged456" }, - base: { ref: "main" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - // Never a CI failure -- the non-required check must not flip ciState/block the PR. - expect(postedBody).not.toContain("`CI failing`"); - expect(postedBody).not.toContain("CI checks failing"); - // But never silently invisible either -- surfaced as its own non-blocking note, with its per-check WHY. - expect(postedBody).toContain("Flagged checks (non-blocking)"); - expect(postedBody).toContain("Contributor trust"); - expect(postedBody).toContain("Manual review needed"); - expect(postedBody).not.toMatch(/wallet|hotkey|reward|trust score/i); - }); - - it("REGRESSION (#4414-class advisory holds): a bare non-required action_required check (no output/details_url) still renders under 'Flagged checks', name-only", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITHUB_PUBLIC_TOKEN: "public-token", GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSignalLevel: "standard", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - checkRunDetailLevel: "minimal", - gateCheckMode: "enabled", reviewCheckMode: "required", - backfillEnabled: true, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 7, - title: "Bump lockfile", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "flagged457" }, - base: { ref: "main" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - return Response.json([ - { uid: 7, githubUsername: "oktofeesh1", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1, hotkey: "must-not-leak" }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") { - return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); - } - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/7/files")) return Response.json([{ filename: "package-lock.json", additions: 2, deletions: 2, status: "modified", patch: "@@\n+1" }]); - if (url.includes("/branches/main/protection/required_status_checks")) return Response.json({ contexts: ["validate", "Superagent Security Scan"] }); - if (url.includes("/check-runs") && method === "GET") { - return Response.json({ - total_count: 3, - check_runs: [ - { name: "validate", status: "completed", conclusion: "success", app: { slug: "github-actions" } }, - { name: "Superagent Security Scan", status: "completed", conclusion: "success", app: { slug: "superagent-security" } }, - // Bare: no output, no details_url -- the common real-world shape for a check-run with nothing to say. - { name: "Contributor trust", status: "completed", conclusion: "action_required", app: { slug: "superagent-security" } }, - ], - }); - } - if (url.includes("/commits/") && url.includes("/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 904 }, { status: 201 }); - if (url.includes("/check-runs/904") && method === "PATCH") return Response.json({ id: 904 }); - if (url.includes("/issues/7/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/7/comments") && method === "POST") { - postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-flagged-nonrequired-check-bare", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 7, - title: "Bump lockfile", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "flagged457" }, - base: { ref: "main" }, - labels: [{ name: "bug" }], - body: "Fixes #1\n\nValidation: npm test", - }, - }, - }); - - expect(postedBody).not.toContain("CI checks failing"); - expect(postedBody).toContain("Flagged checks (non-blocking)"); - expect(postedBody).toContain("- Contributor trust"); - }); - - it("skips bots and maintainer authors, and keeps explicitly enabled checks minimal", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - }); - const calls = { minerList: 0, checks: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", hotkey: "must-not-cache", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/abc123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { output?: { title?: string; text?: string } }; - expect(body.output?.text).toBe("No detailed findings are published in check runs."); - calls.checks += 1; - return Response.json({ id: 99 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - const basePayload = { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - }; - - await processJob(env, { - type: "github-webhook", - deliveryId: "bot-skip", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - pull_request: { number: 20, title: "Dependency update", state: "open", user: { login: "renovate[bot]", type: "Bot" }, labels: [], body: "" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "maintainer-skip", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - pull_request: { number: 21, title: "Maintainer work", state: "open", user: { login: "jsonbored" }, author_association: "OWNER", labels: [], body: "" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "check-enabled", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - pull_request: { number: 22, title: "Miner work", state: "open", user: { login: "oktofeesh1" }, head: { sha: "abc123" }, labels: [], body: "No issue needed." }, - }, - }); - - expect(calls).toEqual({ minerList: 1, checks: 1 }); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ? order by created_at").bind("github_app.pr_visibility_skipped").all<{ - detail: string; - }>(); - expect(skipped.results.map((event) => event.detail)).toEqual(expect.arrayContaining(["bot_author", "maintainer_author"])); - }); - - it("audits advisory context check permission failures without blocking webhook processing", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "off", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor" }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/context403/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs")) return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "context-permission-missing", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 24, title: "Context check", state: "open", user: { login: "contributor" }, head: { sha: "context403" }, labels: [], body: "No issue needed." }, - }, - }); - - const audit = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") - .bind("github_app.check_run_permission_missing") - .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); - - expect(audit).toMatchObject({ - event_type: "github_app.check_run_permission_missing", - actor: "contributor", - target_key: "JSONbored/gittensory#24", - outcome: "error", - }); - expect(audit?.detail).toMatch(/Checks: write permission is missing/i); - }); - - it("audits advisory context check publish failures AND retries the job (GitHub 5xx is transient, GITTENSORY-5)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "off", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor" }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/context500/check-runs")) return new Response("GitHub check API failed", { status: 500 }); - return new Response("not found", { status: 404 }); - }); - const captureSpy = vi.spyOn(sentryModule, "captureReviewFailure"); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "context-check-failure", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 25, title: "Context check", state: "open", user: { login: "contributor" }, head: { sha: "context500" }, labels: [], body: "No issue needed." }, - }, - }), - ).rejects.toMatchObject({ retryKind: "public_surface_publish_transient" }); - - const outputFailure = await env.DB.prepare("select event_type, detail from audit_events where event_type = ?") - .bind("github_app.pr_check_run_publish_failed") - .first<{ event_type: string; detail: string }>(); - expect(outputFailure).toMatchObject({ event_type: "github_app.pr_check_run_publish_failed" }); - expect(outputFailure?.detail).toMatch(/GitHub check API failed|failed/i); - const aggregate = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_failed") - .first<{ detail: string; metadata_json: string }>(); - expect(aggregate).toMatchObject({ detail: "check_run" }); - expect(aggregate?.metadata_json).toContain('"output":"check_run"'); - expect(aggregate?.metadata_json).toContain('"transient":true'); - // The total publish failure (nothing reached the PR) escalates to Sentry at error level, not just the ledger — - // this still fires BEFORE the retryable throw, so the failure stays observable even though the job also retries. - expect(captureSpy).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({ kind: "publish", repo: "JSONbored/gittensory" })); - captureSpy.mockRestore(); - }); - - it("audits disabled public-surface skips without miner lookup", async () => { - const env = createTestEnv(); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - }); - const calls = { fetch: 0, repoWideReads: 0 }; - const originalDb = env.DB; - env.DB = new Proxy(originalDb, { - get(target, prop, receiver) { - if (prop !== "prepare") return Reflect.get(target, prop, receiver); - return (sql: string) => { - if (/from\s+["`]?issues["`]?/i.test(sql) || /from\s+["`]?bounties["`]?/i.test(sql)) calls.repoWideReads += 1; - return target.prepare(sql); - }; - }, - }) as D1Database; - vi.stubGlobal("fetch", async () => { - calls.fetch += 1; - return new Response("unexpected fetch", { status: 500 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", {}); - await processJob(env, { - type: "github-webhook", - deliveryId: "surface-off-skip", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 23, title: "Quiet repo work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "" }, - }, - }); - - expect(calls).toEqual({ fetch: 0, repoWideReads: 0 }); - const skipped = await env.DB.prepare("select actor, target_key, detail, metadata_json from audit_events where event_type = ?").bind("github_app.pr_visibility_skipped").all<{ - actor: string; - target_key: string; - detail: string; - metadata_json: string; - }>(); - expect(skipped.results).toEqual([ - expect.objectContaining({ - actor: "oktofeesh1", - target_key: "JSONbored/gittensory#23", - detail: "surface_off", - }), - ]); - expect(JSON.stringify(skipped.results)).not.toMatch(/wallet|hotkey|raw trust|installation-token/i); - }); - - it("records public comment failure without blocking the context check", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicSurface: "comment_only", - autoLabelEnabled: true, - createMissingLabel: true, - checkRunMode: "enabled", - checkRunDetailLevel: "standard", - }); - const calls = { checks: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/abc123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - calls.checks += 1; - return Response.json({ id: 42, html_url: "https://github.com/checks/42" }, { status: 201 }); - } - if (url.includes("/issues/30/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/30/comments") && method === "POST") return new Response("comment failed", { status: 503 }); - return new Response("not found", { status: 404 }); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "comment-failure", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 30, title: "Miner work", state: "open", head: { sha: "abc123", ref: "feature" }, user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }), - ).resolves.toBeUndefined(); - - expect(calls.checks).toBe(1); - const webhook = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("comment-failure").first<{ status: string }>(); - expect(webhook?.status).toBe("processed"); - const outputFailures = await env.DB.prepare("select event_type, detail from audit_events where target_key = ? and outcome = ? order by event_type") - .bind("JSONbored/gittensory#30", "error") - .all<{ event_type: string; detail: string }>(); - expect(outputFailures.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - event_type: "github_app.pr_comment_publish_failed", - detail: "comment failed", - }), - ]), - ); - const published = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").first<{ metadata_json: string }>(); - expect(published?.metadata_json).toContain('"publishedOutputs":["check_run"]'); - expect(published?.metadata_json).toContain('"output":"comment"'); - }); - - it("records an aggregate public-surface failure when no configured output publishes (permanent failure, no retry)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicSurface: "comment_only", - checkRunMode: "off", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/31/comments") && method === "GET") return Response.json([]); - // A 403 with no rate-limit signal (permissions revoked, not a burst limit) is PERMANENT: retrying forever - // would never converge, so this must keep today's swallow-and-audit behavior, not throw a retryable error. - if (url.includes("/issues/31/comments") && method === "POST") return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); - return new Response("not found", { status: 404 }); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "all-public-outputs-failed", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 31, title: "Miner work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }), - ).resolves.toBeUndefined(); - - const aggregate = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_failed") - .first<{ detail: string; metadata_json: string }>(); - expect(aggregate).toMatchObject({ detail: "comment" }); - expect(aggregate?.metadata_json).toContain('"output":"comment"'); - expect(aggregate?.metadata_json).toContain('"transient":false'); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").all(); - expect(published.results).toEqual([]); - const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("all-public-outputs-failed").first<{ status: string }>(); - expect(webhookRow?.status).toBe("processed"); - }); - - it("retries the whole job when a transient GitHub 5xx drops every public-surface output (GITTENSORY-5)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicSurface: "comment_only", - checkRunMode: "off", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/32/comments") && method === "GET") return Response.json([]); - // GitHub 5xx during publish: momentary, not the caller's fault — the job must retry, not silently drop the - // review the same way JSONbored/awesome-claude#4251 did (Sentry GITTENSORY-5). - if (url.includes("/issues/32/comments") && method === "POST") return new Response("upstream unavailable", { status: 502 }); - return new Response("not found", { status: 404 }); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "transient-publish-failure", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 32, title: "Miner work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }), - ).rejects.toMatchObject({ retryKind: "public_surface_publish_transient" }); - - // The failure IS still audited (observability doesn't regress) — it just also throws so the queue retries. - const aggregate = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_failed") - .first<{ detail: string; metadata_json: string }>(); - expect(aggregate).toMatchObject({ detail: "comment" }); - expect(aggregate?.metadata_json).toContain('"transient":true'); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").all(); - expect(published.results).toEqual([]); - // The webhook row is marked "error", not "processed" — a thrown job is exactly what lets the queue retry it. - const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("transient-publish-failure").first<{ status: string }>(); - expect(webhookRow?.status).toBe("error"); - }); - - it("leaves a fully successful public-surface publish unaffected by the transient-retry check", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicSurface: "comment_only", - checkRunMode: "off", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 2, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/33/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/33/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "public-surface-clean-publish", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 33, title: "Miner work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }), - ).resolves.toBeUndefined(); - - const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("public-surface-clean-publish").first<{ status: string }>(); - expect(webhookRow?.status).toBe("processed"); - const failed = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.pr_public_surface_failed").all(); - expect(failed.results).toEqual([]); - const published = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").first<{ metadata_json: string }>(); - expect(published?.metadata_json).toContain('"publishedOutputs":["comment"]'); - expect(published?.metadata_json).toContain('"failedOutputs":[]'); - }); - - it("keeps repository and PR webhook processing internal when installation context is absent", async () => { - const env = createTestEnv(); - await processJob(env, { - type: "github-webhook", - deliveryId: "repositories-without-installation", - eventName: "repository", - payload: { - action: "created", - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }], - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "pr-without-installation", - eventName: "pull_request", - payload: { - action: "opened", - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 44, title: "Internal-only PR", state: "open", user: { login: "oktofeesh1" }, labels: [] }, - }, - }); - - expect(await listPullRequests(env, "JSONbored/gittensory")).toEqual(expect.arrayContaining([expect.objectContaining({ number: 44, body: null })])); - const events = await env.DB.prepare("select delivery_id, status from webhook_events where delivery_id in (?, ?) order by delivery_id") - .bind("pr-without-installation", "repositories-without-installation") - .all<{ delivery_id: string; status: string }>(); - expect(events.results).toEqual([ - { delivery_id: "pr-without-installation", status: "processed" }, - { delivery_id: "repositories-without-installation", status: "processed" }, - ]); - }); - - it("uses cached confirmed miner detection for label-only public surfaces", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - }); - const calls = { comments: 0, labels: 0, minerList: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/comments")) { - calls.comments += 1; - return Response.json([]); - } - if (url.includes("/labels") && method === "GET") return Response.json([]); - if (url.includes("/labels") && method === "POST") { - calls.labels += 1; - return Response.json([{ name: "gittensor" }]); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "label-only", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 45, title: "Miner label-only work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "label-only-cached", - eventName: "pull_request", - payload: { - action: "synchronize", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 46, title: "Miner label-only follow-up", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }); - - // 2 PRs × 3 label POSTs each: the gittensor context label (apply) + the per-PR TYPE label (create + apply). - expect(calls).toEqual({ comments: 0, labels: 6, minerList: 1 }); - const cacheAudit = await env.DB.prepare("select event_type, detail from audit_events where actor = ? order by created_at") - .bind("oktofeesh1") - .all<{ event_type: string; detail: string | null }>(); - expect(cacheAudit.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), - expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "confirmed" }), - ]), - ); - const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("oktofeesh1").first<{ status: string }>(); - expect(cached?.status).toBe("confirmed"); - const snapshot = await env.DB.prepare("select snapshot_json from official_miner_detections where login = ?").bind("oktofeesh1").first<{ snapshot_json: string }>(); - expect(snapshot?.snapshot_json).not.toContain("must-not-cache"); - }); - - it("records label-only public-surface failures without creating duplicate comments", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - }); - const calls = { comments: 0, labels: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/comments")) { - calls.comments += 1; - return Response.json([]); - } - if (url.includes("/labels") && method === "GET") return Response.json([]); - if (url.includes("/labels") && method === "POST") { - calls.labels += 1; - // A permanent failure (permissions gap, not a momentary blip) — this test is about duplicate-comment - // suppression on a label-only surface, not about retry classification, so it must stay non-transient. - return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 }); - } - return new Response("not found", { status: 404 }); - }); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "label-failure", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 50, title: "Miner label work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }), - ).resolves.toBeUndefined(); - - // gittensor context-label apply (fails 403, recorded) + the best-effort type-label create attempt (also 403, - // swallowed). The context-label failure is still recorded below; the type label never drops the recording. - expect(calls).toEqual({ comments: 0, labels: 2 }); - const outputFailure = await env.DB.prepare("select event_type, detail from audit_events where event_type = ?") - .bind("github_app.pr_label_publish_failed") - .first<{ event_type: string; detail: string }>(); - expect(outputFailure?.event_type).toBe("github_app.pr_label_publish_failed"); - expect(outputFailure?.detail).toMatch(/Resource not accessible by integration/); - const aggregate = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.pr_public_surface_failed") - .first<{ detail: string; metadata_json: string }>(); - expect(aggregate).toMatchObject({ detail: "label" }); - expect(aggregate?.metadata_json).toContain('"output":"label"'); - const published = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.pr_public_surface_published").all(); - expect(published.results).toEqual([]); - }); - - it("keeps GitHub-history-only contributors quiet through not_found cache hits and expiry", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 9, - title: "Historical merged work", - state: "closed", - merged_at: "2026-05-22T00:00:00.000Z", - user: { login: "newbie" }, - author_association: "NONE", - labels: [{ name: "feature" }], - body: "Previously merged.", - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicAudienceMode: "gittensor_only", - publicSurface: "comment_and_label", - autoLabelEnabled: true, - checkRunMode: "off", - }); - const calls = { minerList: 0, publicOutput: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([]); - } - if (url.includes("/access_tokens") || url.includes("/comments") || url.includes("/labels")) { - calls.publicOutput += 1; - return Response.json({}); - } - return new Response("not found", { status: 404 }); - }); - const basePayload = { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - }; - - for (const number of [47, 48]) { - await processJob(env, { - type: "github-webhook", - deliveryId: `not-found-cache-${number}`, - eventName: "pull_request", - payload: { - ...basePayload, - pull_request: { number, title: "Contributor work", state: "open", user: { login: "newbie" }, labels: [], body: "Fixes #1" }, - }, - }); - } - await env.DB.prepare("update official_miner_detections set expires_at = ? where login = ?").bind("2000-01-01T00:00:00.000Z", "newbie").run(); - await processJob(env, { - type: "github-webhook", - deliveryId: "not-found-cache-expired", - eventName: "pull_request", - payload: { - ...basePayload, - pull_request: { number: 49, title: "Contributor follow-up", state: "open", user: { login: "newbie" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(calls).toEqual({ minerList: 2, publicOutput: 0 }); - const audit = await env.DB.prepare("select event_type, detail from audit_events where actor = ? order by created_at") - .bind("newbie") - .all<{ event_type: string; detail: string | null }>(); - expect(audit.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), - expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "not_found" }), - expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", detail: "not_official_gittensor_miner" }), - ]), - ); - const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("newbie").first<{ status: string }>(); - expect(cached?.status).toBe("not_found"); - }); - - it("checks official miner status for detected-only comments before publishing public output", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicAudienceMode: "oss_maintainer", - publicSurface: "comment_only", - autoLabelEnabled: false, - checkRunMode: "off", - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 3, - title: "Cached historical work", - state: "closed", - merged_at: "2026-05-20T00:00:00.000Z", - user: { login: "confirmed-dev" }, - labels: [], - body: "Historical cached PR.", - }); - - const calls = { minerList: 0, comments: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([ - { githubUsername: "confirmed-dev", githubId: "123", totalPrs: 2, totalMergedPrs: 1, isEligible: true, credibility: 1 }, - ]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/confirmed-dev")) return Response.json({ login: "confirmed-dev", public_repos: 1, followers: 0 }); - if (url.includes("/users/confirmed-dev/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/51/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/51/comments") && method === "POST") { - calls.comments += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; - expect(body.body).toContain("[Gittensor profile](https://gittensor.io/miners/details?githubId=123)"); - expect(body.body).toContain("2 PR(s)"); - expect(body.body).not.toContain("Cached prior PRs/issues"); - expect(body.body).not.toContain("api.gittensor.io/miners/123"); - return Response.json({ id: 51 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - const basePayload = { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - }; - - await processJob(env, { - type: "github-webhook", - deliveryId: "detected-comment-confirmed", - eventName: "pull_request", - payload: { - ...basePayload, - pull_request: { number: 51, title: "Confirmed contributor work", state: "open", user: { login: "confirmed-dev" }, labels: [], body: "Fixes #1" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "detected-comment-not-found", - eventName: "pull_request", - payload: { - ...basePayload, - pull_request: { number: 52, title: "Unconfirmed contributor work", state: "open", user: { login: "newbie" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(calls).toEqual({ minerList: 2, comments: 2 }); - }); - - it("fails closed when official miner detection is unavailable", async () => { - const env = createTestEnv(); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - publicAudienceMode: "gittensor_only", - }); - const payload = { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { - number: 10, - title: "Check run failure path", - state: "open", - user: { login: "oktofeesh1" }, - head: { sha: "abc123" }, - labels: [], - body: "Fixes #1", - }, - }; - - const calls = { minerList: 0 }; - vi.stubGlobal("fetch", async () => { - calls.minerList += 1; - return new Response("gittensor unavailable", { status: 503 }); - }); - - await upsertRepoFocusManifest(env, "JSONbored/gittensory", {}); - await expect(processJob(env, { type: "github-webhook", deliveryId: "miner-unavailable", eventName: "pull_request", payload })).resolves.toBeUndefined(); - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "miner-unavailable-cached", - eventName: "pull_request", - payload: { ...payload, pull_request: { ...payload.pull_request, number: 11 } }, - }), - ).resolves.toBeUndefined(); - expect(calls.minerList).toBe(1); - const audit = await env.DB.prepare("select event_type, outcome, detail from audit_events where target_key = ?") - .bind("JSONbored/gittensory#10") - .all<{ event_type: string; outcome: string; detail: string }>(); - expect(audit.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", outcome: "completed", detail: "miss" }), - expect.objectContaining({ event_type: "github_app.miner_detection_unavailable", outcome: "error", detail: expect.stringContaining("Gittensor API failed") }), - expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", outcome: "completed", detail: "miner_detection_unavailable" }), - ]), - ); - const cachedAudit = await env.DB.prepare("select event_type, outcome, detail from audit_events where target_key = ?") - .bind("JSONbored/gittensory#11") - .all<{ event_type: string; outcome: string; detail: string }>(); - expect(cachedAudit.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", outcome: "completed", detail: "unavailable" }), - expect.objectContaining({ event_type: "github_app.miner_detection_unavailable", outcome: "error", detail: expect.stringContaining("Gittensor API failed") }), - expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", outcome: "completed", detail: "miner_detection_unavailable" }), - ]), - ); - const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("oktofeesh1").first<{ status: string }>(); - expect(cached?.status).toBe("unavailable"); - }); - - it("recovers confirmed miners after the unavailable cache window expires", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "detected_contributors_only", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - }); - let officialSource: "down" | "confirmed" = "down"; - const calls = { minerList: 0, labels: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - if (officialSource === "down") return new Response("gittensor unavailable", { status: 503 }); - return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", hotkey: "must-not-cache", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/labels") && method === "GET") return Response.json([]); - if (url.includes("/labels") && method === "POST") { - calls.labels += 1; - return Response.json([{ name: "gittensor" }]); - } - return new Response("not found", { status: 404 }); - }); - const basePayload = { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - }; - - for (const number of [12, 13]) { - await processJob(env, { - type: "github-webhook", - deliveryId: `miner-unavailable-recovery-${number}`, - eventName: "pull_request", - payload: { - ...basePayload, - pull_request: { number, title: "Miner recovery", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }); - } - expect(calls).toEqual({ minerList: 1, labels: 0 }); - await env.DB.prepare("update official_miner_detections set expires_at = ? where login = ?").bind("2000-01-01T00:00:00.000Z", "oktofeesh1").run(); - officialSource = "confirmed"; - - await processJob(env, { - type: "github-webhook", - deliveryId: "miner-unavailable-recovered", - eventName: "pull_request", - payload: { - ...basePayload, - pull_request: { number: 14, title: "Miner recovery confirmed", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, - }, - }); - - // 1 labeled PR × 3 label POSTs: the gittensor context label (apply) + the per-PR TYPE label (create + apply). - expect(calls).toEqual({ minerList: 2, labels: 3 }); - const cached = await env.DB.prepare("select status, snapshot_json from official_miner_detections where login = ?") - .bind("oktofeesh1") - .first<{ status: string; snapshot_json: string }>(); - expect(cached?.status).toBe("confirmed"); - expect(cached?.snapshot_json).not.toMatch(/hotkey|wallet|coldkey|must-not-cache/i); - }); - - it("suppresses labels and comments when agentPaused is true", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload( - { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, - { kind: "raw-github", url: "https://example.test" }, - "2026-05-23T00:00:00.000Z", - ), - ); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - publicSurface: "comment_and_label", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - agentPaused: true, - }); - const calls = { labels: 0, comments: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "paused-miner", githubId: "999", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); - if (url === "https://api.gittensor.io/miners/999") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/999/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/999/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/paused-miner")) return Response.json({ login: "paused-miner" }); - if (url.includes("/users/paused-miner/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/labels") && method === "POST") { - calls.labels += 1; - return Response.json([{ name: "gittensor" }]); - } - if (url.includes("/comments") && method === "POST") { - calls.comments += 1; - return Response.json({ id: 1 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "paused-surface", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, - pull_request: { number: 88, title: "Paused repo PR", state: "open", user: { login: "paused-miner" }, labels: [], body: "Fixes #1" }, - }, - }); - - // agentPaused suppresses ALL public surface mutations — no label, no comment. - expect(calls).toEqual({ labels: 0, comments: 0 }); - }); - - it("responds to authorized @gittensory mention commands with one public-safe comment", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 77, - title: "Miner command context", - state: "open", - user: { login: "oktofeesh1" }, - author_association: "NONE", - labels: [], - body: "Fixes #1", - }); - const calls = { commentsCreated: 0, token: 0, minerList: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 3, totalMergedPrs: 2, isEligible: true, credibility: 1 }]); - } - if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); - if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 3, followers: 1 }); - if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - // #788: Q&A commands now authorize by REAL repo permission. The "maintainer" commenter has maintain - // access; everyone else has none and is authorized only as pr_author/confirmed_miner where applicable. - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); - if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { - calls.commentsCreated += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; - expect(body.body).toContain(""); - expect(body.body).toContain("@gittensory"); - expect(body.body).not.toMatch(/wallet|hotkey|estimated score|reward estimate|payout|farming|raw trust score|private reviewability|reviewability internals|scoreability|public score estimate/i); - return Response.json({ id: 1001 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-miner-context", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 1, - body: "@gittensory miner-context", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-blockers", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 2, - body: "@gittensory blockers", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-help", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 3, - body: "@gittensory help", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-author-next-action", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 4, - body: "@gittensory next-action", - user: { login: "oktofeesh1", type: "User" }, - author_association: "NONE", - }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-reviewability", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 5, - body: "@gittensory reviewability", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-repo-fit", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 6, - body: "@gittensory repo-fit", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-packet", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 7, - body: "@gittensory packet", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-packet-no-cache", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 78, title: "Uncached PR command", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 8, - body: "@gittensory packet", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - - expect(calls.commentsCreated).toBe(8); - // The installation token is cached + reused across all 8 commands (each previously minted 2 — permission - // check + comment — for 16 total). Caching collapses them to a single mint, which is the rate-limit fix. - expect(calls.token).toBe(1); - expect(calls.minerList).toBeGreaterThanOrEqual(1); - const audit = await env.DB.prepare("select event_type, detail from audit_events where target_key = ? order by created_at") - .bind("JSONbored/gittensory#77") - .all<{ event_type: string; detail: string | null }>(); - expect(audit.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ event_type: "github_app.agent_command_replied" }), - expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), - expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "confirmed" }), - ]), - ); - const usage = await env.DB.prepare("select payload_json from signal_snapshots where signal_type = ? and target_key = ? order by generated_at") - .bind("github-agent-command-usage", "JSONbored/gittensory#77") - .all<{ payload_json: string }>(); - const usagePayloads = usage.results.map((entry) => JSON.parse(entry.payload_json) as { command: string; outcome: string; actorKind: string; actorHash?: string }); - expect(usagePayloads).toEqual( - expect.arrayContaining([ - expect.objectContaining({ command: "reviewability", outcome: "replied", actorKind: "maintainer" }), - expect.objectContaining({ command: "repo-fit", outcome: "replied", actorKind: "maintainer" }), - expect.objectContaining({ command: "packet", outcome: "replied", actorKind: "maintainer" }), - ]), - ); - expect(usagePayloads.every((payload) => typeof payload.actorHash === "string" && /^[a-f0-9]{64}$/.test(payload.actorHash))).toBe(true); - expect(JSON.stringify(usagePayloads)).not.toContain('"actor":'); - expect(JSON.stringify(usagePayloads)).not.toMatch(/wallet|hotkey|raw trust score|payout|reward estimate|farming|private reviewability|public score estimate|@gittensory|oktofeesh1/i); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ surface: "github_app", eventName: "agent_command_replied", outcome: "completed", repoFullName: "JSONbored/gittensory" }), - ]), - ); - expect(JSON.stringify(usageEvents)).not.toMatch(/wallet|hotkey|raw trust|deliveryId|installation-token/i); - }); - - it("a @gittensory Q&A mention command respects agentPaused — never posts the answer card live (#2258)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 77, - title: "Paused Q&A context", - state: "open", - user: { login: "oktofeesh1" }, - author_association: "NONE", - labels: [], - body: "Fixes #1", - }); - const calls = { commentPosts: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); - if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { - calls.commentPosts += 1; - return Response.json({ id: 1001 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-help-paused", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Paused Q&A context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { id: 1, body: "@gittensory help", user: { login: "maintainer", type: "User" }, author_association: "OWNER" }, - }, - }); - - expect(calls.commentPosts).toBe(0); // the answer card must never post live on a paused repo - // REGRESSION: a paused command must not be audited/usage-tracked as a real, completed reply. - const replied = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.agent_command_replied").first<{ id: string }>(); - expect(replied).toBeUndefined(); - const skipped = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.agent_command_reply_skipped").first<{ outcome: string; detail: string }>(); - expect(skipped).toMatchObject({ outcome: "completed", detail: "agent_paused" }); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ eventName: "agent_command_reply_skipped", outcome: "skipped" })])); - expect(usageEvents.some((event) => event.eventName === "agent_command_replied")).toBe(false); - }); - - it("a @gittensory maintainer-digest command respects agentDryRun — records dry_run, not agent_paused (#2258)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 78, - title: "Dry-run digest context", - state: "open", - user: { login: "oktofeesh1" }, - author_association: "NONE", - labels: [], - body: "Fixes #1", - }); - const calls = { commentPosts: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); - if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { - calls.commentPosts += 1; - return Response.json({ id: 1002 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-queue-summary-dry-run", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 78, title: "Dry-run digest context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { id: 2, body: "@gittensory queue-summary", user: { login: "maintainer", type: "User" }, author_association: "OWNER" }, - }, - }); - - expect(calls.commentPosts).toBe(0); // the digest must never post live on a dry-run repo - const skipped = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.agent_command_reply_skipped") - .first<{ outcome: string; detail: string; metadata_json: string }>(); - expect(skipped).toMatchObject({ outcome: "completed", detail: "dry_run" }); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual( - expect.arrayContaining([expect.objectContaining({ eventName: "agent_command_reply_skipped", outcome: "skipped", metadata: expect.objectContaining({ family: "queue_digest" }) })]), - ); - }); - - it("posts maintainer-only queue digest commands from cached public-safe metadata", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - delete (env as Partial).PUBLIC_SITE_ORIGIN; - for (const issue of [ - { number: 1, title: "Ready linked fix" }, - { number: 2, title: "Overlap issue" }, - ]) { - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { - number: issue.number, - title: issue.title, - state: "open", - user: { login: "reporter" }, - labels: [], - body: "", - }); - } - for (const pull of [ - { number: 90, title: "Ready linked fix", user: { login: "alice" }, body: "Fixes #1" }, - { number: 91, title: "Needs author context", user: { login: "bob" }, body: "" }, - { number: 92, title: "Overlap route first", user: { login: "carol" }, body: "Fixes #2" }, - { number: 93, title: "Overlap route second", user: { login: "dana" }, body: "Fixes #2" }, - ]) { - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - ...pull, - state: "open", - author_association: "NONE", - labels: [], - }); - } - await upsertOfficialMinerDetection(env, "alice", { status: "confirmed", snapshot: queueMinerSnapshot("alice") }, 60_000); - - const calls = { commentsCreated: 0, token: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); // #788 real-permission auth - if (url.includes("/issues/90/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/90/comments") && method === "POST") { - calls.commentsCreated += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; - expect(body.body).toContain("**Gittensory maintainer queue summary**"); - expect(body.body).toContain("Open PRs: 4"); - expect(body.body).toContain("confirmed-miner PRs: 1"); - expect(body.body).toContain("Authenticated control panel: https://gittensory.aethereal.dev/app?view=maintainer&repo=JSONbored%2Fgittensory"); - expect(body.body).not.toMatch(/wallet|hotkey|raw trust score|payout|reward estimate|farming|private reviewability|public score estimate/i); - return Response.json({ id: 1001 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "maintainer-queue-summary", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 90, title: "Ready linked fix", state: "open", pull_request: {}, user: { login: "alice" }, author_association: "NONE" }, - comment: { - id: 9001, - body: "@gittensory queue-summary", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - - expect(calls).toEqual({ commentsCreated: 1, token: 1 }); // token cached + reused across the #788 permission check - const audit = await env.DB.prepare("select event_type, detail, metadata_json from audit_events where target_key = ? order by created_at") - .bind("JSONbored/gittensory#90") - .all<{ event_type: string; detail: string | null; metadata_json: string }>(); - expect(audit.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ event_type: "github_app.agent_command_replied" }), - expect.objectContaining({ event_type: "github_app.agent_command_feedback_prompted", detail: "queue-summary" }), - ]), - ); - expect(audit.results.find((entry) => entry.event_type === "github_app.agent_command_feedback_prompted")?.metadata_json).toContain("maintainer_digest"); - const usage = await env.DB.prepare("select payload_json from signal_snapshots where signal_type = ? and target_key = ?") - .bind("github-agent-command-usage", "JSONbored/gittensory#90") - .all<{ payload_json: string }>(); - const usagePayload = JSON.parse(usage.results[0]?.payload_json ?? "{}") as { command?: string; outcome?: string; family?: string; actorHash?: string }; - expect(usagePayload).toEqual(expect.objectContaining({ command: "queue-summary", outcome: "replied", family: "maintainer_digest" })); - expect(usagePayload.actorHash).toMatch(/^[a-f0-9]{64}$/); - expect(JSON.stringify(usagePayload)).not.toContain('"actor":'); - const usageEvents = await listProductUsageEvents(env, { limit: 5 }); - expect(usageEvents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ surface: "github_app", eventName: "agent_command_replied", outcome: "completed", metadata: expect.objectContaining({ family: "queue_digest" }) }), - ]), - ); - }); - - it("omits the maintainer queue digest control-panel link when the public site origin is invalid", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), PUBLIC_SITE_ORIGIN: "not a url" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 94, - title: "Ready linked fix", - state: "open", - author_association: "NONE", - user: { login: "alice" }, - labels: [], - body: "Fixes #1", - }); - let commentBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); // #788 real-permission auth - if (url.includes("/issues/94/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/94/comments") && method === "POST") { - const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; - commentBody = body.body ?? ""; - return Response.json({ id: 1002 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "maintainer-queue-summary-invalid-origin", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 94, title: "Ready linked fix", state: "open", pull_request: {}, user: { login: "alice" }, author_association: "NONE" }, - comment: { - id: 9002, - body: "@gittensory queue-summary", - user: { login: "maintainer", type: "User" }, - author_association: "OWNER", - }, - }, - }); - - expect(commentBody).toContain("**Gittensory maintainer queue summary**"); - expect(commentBody).not.toContain("Authenticated control panel:"); - }); - - it("applies repo command authorization policy overrides during issue_comment handling", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commandAuthorization: { default: ["maintainer"], commands: { help: ["pr_author"] } }, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 91, - title: "Author policy command", - state: "open", - user: { login: "driveby" }, - author_association: "NONE", - labels: [], - body: "Fixes #90", - }); - - const calls = { commentsCreated: 0, token: 0, minerList: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - calls.minerList += 1; - return Response.json([]); - } - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - // #788: "driveby" has no repo permission — authorized only as pr_author via the help-command override. - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); - if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { - calls.commentsCreated += 1; - const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; - expect(body.body).toContain(""); - expect(body.body).not.toMatch(/wallet|hotkey|estimated score|reward estimate|payout|farming|raw trust score|private reviewability|public score estimate/i); - return Response.json({ id: 9191 }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-policy-author", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 91, title: "Author policy command", state: "open", pull_request: {}, user: { login: "driveby" }, author_association: "NONE" }, - comment: { - id: 191, - body: "@gittensory help", - user: { login: "driveby", type: "User" }, - author_association: "NONE", - }, - }, - }); - - expect(calls).toEqual({ commentsCreated: 1, token: 1, minerList: 0 }); // token cached + reused across the #788 permission check - const audit = await env.DB.prepare("select event_type, detail from audit_events where target_key = ? order by created_at") - .bind("JSONbored/gittensory#91") - .all<{ event_type: string; detail: string | null }>(); - expect(audit.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ event_type: "github_app.agent_command_replied", detail: null }), - expect.objectContaining({ event_type: "github_app.agent_command_feedback_prompted", detail: "help" }), - ]), - ); - const usage = await env.DB.prepare("select payload_json from signal_snapshots where signal_type = ? and target_key = ?") - .bind("github-agent-command-usage", "JSONbored/gittensory#91") - .all<{ payload_json: string }>(); - expect(JSON.parse(usage.results[0]?.payload_json ?? "{}")).toMatchObject({ command: "help", outcome: "replied", actorKind: "author" }); - }); - - it("records deduped @gittensory answer usefulness from authorized reactions only", async () => { - const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "maintainer" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 77, - title: "Miner command context", - state: "open", - user: { login: "oktofeesh1" }, - author_association: "NONE", - }); - await upsertOfficialMinerDetection(env, "oktofeesh1", { status: "confirmed", snapshot: queueMinerSnapshot("oktofeesh1") }, 60 * 60 * 1000); - await upsertAgentCommandAnswer(env, commandAnswer("answer-maintainer", "preflight", { responseCommentId: 9001 })); - await upsertAgentCommandAnswer(env, commandAnswer("answer-author", "next-action", { responseCommentId: 9002 })); - await upsertAgentCommandAnswer(env, { ...commandAnswer("answer-no-author", "preflight", { responseCommentId: 9003 }), issueNumber: 78 }); - const basePayload = { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - }; - - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-1", - eventName: "reaction", - payload: { - ...basePayload, - comment: { id: 9001, body: commandAnswerBody("answer-maintainer", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 1, content: "+1", user: { login: "maintainer", type: "User" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-2", - eventName: "reaction", - payload: { - ...basePayload, - comment: { id: 9001, body: commandAnswerBody("answer-maintainer", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 2, content: "-1", user: { login: "maintainer", type: "User" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-3", - eventName: "reaction", - payload: { - ...basePayload, - comment: { id: 9002, body: commandAnswerBody("answer-author", "next-action"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 3, content: "+1", user: { login: "oktofeesh1", type: "User" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-4", - eventName: "reaction", - payload: { - ...basePayload, - comment: { id: 9002, body: commandAnswerBody("answer-author", "next-action"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 4, content: "+1", user: { login: "random", type: "User" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-5", - eventName: "reaction", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 78, title: "No author", state: "open", pull_request: {}, author_association: "NONE" }, - comment: { id: 9003, body: commandAnswerBody("answer-no-author", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 5, content: "+1", user: { login: "random", type: "User" } }, - }, - }); - - const summary = await getCommandUsefulnessSummary(env, { now: "2026-05-29T00:00:00.000Z", windowDays: 30 }); - expect(summary.totals).toMatchObject({ feedbackCount: 2, usefulCount: 1, notUsefulCount: 1, answerCount: 2, usefulnessRate: 0.5 }); - expect(summary.commands).toEqual([ - expect.objectContaining({ command: "next-action", feedbackCount: 1, usefulCount: 1 }), - expect.objectContaining({ command: "preflight", feedbackCount: 1, notUsefulCount: 1 }), - ]); - const audit = await env.DB.prepare("select event_type, detail from audit_events where event_type like ? order by created_at") - .bind("github_app.agent_command_feedback_%") - .all<{ event_type: string; detail: string | null }>(); - expect(audit.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ event_type: "github_app.agent_command_feedback_recorded" }), - expect.objectContaining({ event_type: "github_app.agent_command_feedback_denied", detail: "not_maintainer_or_pr_author" }), - ]), - ); - const stored = await env.DB.prepare("select actor_hash, metadata_json from github_agent_command_feedback").all<{ actor_hash: string; metadata_json: string }>(); - expect(stored.results.map((row) => row.actor_hash).join("\n")).not.toMatch(/maintainer|oktofeesh1|random/); - expect(stored.results.every((row) => row.actor_hash.startsWith("sha256:"))).toBe(true); - }); - - it("skips unsupported @gittensory feedback reactions without storing votes", async () => { - const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "maintainer" }); - await upsertAgentCommandAnswer(env, commandAnswer("answer-skip", "preflight", { responseCommentId: 9001 })); - const basePayload = { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - }; - - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-skip-1", - eventName: "reaction", - payload: { - ...basePayload, - action: "deleted", - comment: { id: 9001, body: commandAnswerBody("answer-skip", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 1, content: "+1", user: { login: "maintainer", type: "User" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-skip-1b", - eventName: "reaction", - payload: { - ...basePayload, - comment: { id: 9001, body: commandAnswerBody("answer-skip", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 11, content: "+1", user: { login: "maintainer", type: "User" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-skip-2", - eventName: "reaction", - payload: { - ...basePayload, - action: "created", - comment: { id: 9001, body: commandAnswerBody("answer-skip", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 2, content: "+1", user: { login: "helper[bot]", type: "Bot" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-skip-3", - eventName: "reaction", - payload: { - ...basePayload, - action: "created", - comment: { id: 9001, body: commandAnswerBody("answer-missing", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - reaction: { id: 3, content: "-1", user: { login: "maintainer", type: "User" } }, - }, - }); - - await expect(getCommandUsefulnessSummary(env, { now: "2026-05-29T00:00:00.000Z", windowDays: 30 })).resolves.toMatchObject({ - totals: { feedbackCount: 0 }, - commands: [], - }); - const skips = await env.DB.prepare("select detail from audit_events where event_type = ? order by detail") - .bind("github_app.agent_command_feedback_skipped") - .all<{ detail: string }>(); - expect(skips.results.map((row) => row.detail)).toEqual(["bot_reaction", "unknown_answer", "unsupported_reaction_action", "unsupported_reaction_action"]); - }); - - it("accepts repo-owner feedback through sender fallback and ignores non-vote reactions", async () => { - const env = createTestEnv(); - await upsertAgentCommandAnswer(env, commandAnswer("answer-owner", "blockers")); - const basePayload = { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { id: 9001, body: commandAnswerBody("answer-owner", "blockers"), user: { login: "gittensory[bot]", type: "Bot" } }, - }; - - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-owner-1", - eventName: "reaction", - payload: { - ...basePayload, - reaction: { content: "+1" }, - sender: { login: "JSONbored", type: "User" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-owner-2", - eventName: "reaction", - payload: { - ...basePayload, - reaction: { id: 2, content: "heart" }, - sender: { login: "JSONbored", type: "User" }, - }, - }); - - const summary = await getCommandUsefulnessSummary(env, { now: "2026-05-29T00:00:00.000Z", windowDays: 30 }); - expect(summary.totals).toMatchObject({ feedbackCount: 1, usefulCount: 1, answerCount: 1 }); - expect(summary.commands).toEqual([expect.objectContaining({ command: "blockers", usefulnessRate: 1 })]); - }); - - it("rejects copied feedback markers that do not match the stored answer context", async () => { - const env = createTestEnv({ ADMIN_GITHUB_LOGINS: "maintainer" }); - await upsertAgentCommandAnswer(env, commandAnswer("answer-bound", "preflight", { responseCommentId: 9001 })); - const basePayload = { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - reaction: { id: 1, content: "+1", user: { login: "maintainer", type: "User" } }, - }; - - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-bound-1", - eventName: "reaction", - payload: { - ...basePayload, - comment: { id: 9002, body: commandAnswerBody("answer-bound", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-bound-2", - eventName: "reaction", - payload: { - ...basePayload, - repository: { name: "other", full_name: "JSONbored/other", private: false, owner: { login: "JSONbored" } }, - comment: { id: 9001, body: commandAnswerBody("answer-bound", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "feedback-bound-3", - eventName: "reaction", - payload: { - ...basePayload, - issue: { ...basePayload.issue, number: 78 }, - comment: { id: 9001, body: commandAnswerBody("answer-bound", "preflight"), user: { login: "gittensory[bot]", type: "Bot" } }, - }, - }); - - await expect(getCommandUsefulnessSummary(env, { now: "2026-05-29T00:00:00.000Z", windowDays: 30 })).resolves.toMatchObject({ - totals: { feedbackCount: 0 }, - commands: [], - }); - const skips = await env.DB.prepare("select detail from audit_events where event_type = ? order by detail") - .bind("github_app.agent_command_feedback_skipped") - .all<{ detail: string }>(); - expect(skips.results.map((row) => row.detail)).toEqual(["answer_comment_mismatch", "answer_context_mismatch", "answer_context_mismatch"]); - }); - - it("records webhook errors when command replies fail before mutation", async () => { - const env = createTestEnv(); - // Authorize via the confirmed-miner path (PR author + cached confirmed status), which does not depend on - // the #788 real-permission check — that check swallows the invalid-repo error and would deny otherwise. - await upsertOfficialMinerDetection(env, "oktofeesh1", { status: "confirmed", snapshot: queueMinerSnapshot("oktofeesh1") }, 60_000); - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-error", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "broken", full_name: "broken", private: false, owner: { login: "JSONbored" } }, - issue: { number: 77, title: "Broken command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { - id: 9, - body: "@gittensory help", - user: { login: "oktofeesh1", type: "User" }, - author_association: "NONE", - }, - }, - }), - ).rejects.toThrow("Invalid repository full name"); - - const event = await env.DB.prepare("select status, error_summary from webhook_events where delivery_id = ?") - .bind("agent-command-error") - .first<{ status: string; error_summary: string }>(); - expect(event).toMatchObject({ status: "error", error_summary: expect.stringContaining("Invalid repository full name") }); - }); - - it("skips unauthorized, bot, and non-PR @gittensory mention commands without public output", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - let commentCalls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/issues/")) { - commentCalls += 1; - return Response.json([]); - } - return new Response("not found", { status: 404 }); - }); - const basePayload = { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - }; - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-none", - eventName: "issue_comment", - payload: { - ...basePayload, - issue: { number: 79, title: "No command", state: "open", pull_request: {}, user: { login: "reporter" } }, - comment: { id: 0, body: "plain comment", user: { login: "reporter", type: "User" }, author_association: "NONE" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-missing-fields", - eventName: "issue_comment", - payload: { - action: "created", - comment: { id: 9, body: "@gittensory preflight", user: { login: "reporter", type: "User" }, author_association: "NONE" }, - }, - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-non-pr", - eventName: "issue_comment", - payload: { - ...basePayload, - issue: { number: 80, title: "Plain issue", state: "open", user: { login: "reporter" } }, - comment: { id: 1, body: "@gittensory preflight", user: { login: "reporter", type: "User" }, author_association: "NONE" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-bot", - eventName: "issue_comment", - payload: { - ...basePayload, - issue: { number: 81, title: "Bot PR", state: "open", pull_request: {}, user: { login: "renovate[bot]" } }, - comment: { id: 2, body: "@gittensory preflight", user: { login: "renovate[bot]", type: "Bot" }, author_association: "NONE" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-unauthorized", - eventName: "issue_comment", - payload: { - ...basePayload, - issue: { number: 82, title: "Unauthorized PR", state: "open", pull_request: {}, user: { login: "not-a-miner" }, author_association: "NONE" }, - comment: { id: 3, body: "@gittensory preflight", user: { login: "not-a-miner", type: "User" }, author_association: "NONE" }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-no-pr-author", - eventName: "issue_comment", - payload: { - ...basePayload, - issue: { number: 83, title: "Unknown author PR", state: "open", pull_request: {}, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory preflight", user: { login: "commenter", type: "User" } }, - }, - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-maintainer-only-denied", - eventName: "issue_comment", - payload: { - ...basePayload, - issue: { number: 84, title: "Maintainer digest PR", state: "open", pull_request: {}, user: { login: "not-a-miner" }, author_association: "NONE" }, - comment: { id: 5, body: "@gittensory queue-summary", user: { login: "not-a-miner", type: "User" }, author_association: "NONE" }, - }, - }); - - expect(commentCalls).toBe(0); - const skips = await env.DB.prepare("select detail from audit_events where event_type = ? order by detail") - .bind("github_app.agent_command_skipped") - .all<{ detail: string }>(); - expect(skips.results.map((entry) => entry.detail)).toEqual(expect.arrayContaining(["bot_author", "maintainer_command_requires_maintainer", "not_a_pull_request_thread", "pr_author_not_confirmed_miner"])); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ surface: "github_app", eventName: "agent_command_skipped", outcome: "skipped" }), - ]), - ); - expect(JSON.stringify(usageEvents)).not.toMatch(/deliveryId|wallet|hotkey|raw trust/i); - }); - - describe("review-nag cooldown (#2463)", () => { - // Reusable stub covering everything the normal @gittensory Q&A dispatch needs (token, collaborator - // permission, comment GET/search + POST) PLUS the maintenance close path (label GET/POST, PR PATCH) — - // a superset so every scenario below (fall-through OR short-circuit) can share one fetch handler. - function stubReviewNagFetch(prNumber: number, seen: { comments: string[]; labels: string[]; closed: boolean }) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); - if (url.endsWith(`/pulls/${prNumber}`) && method === "PATCH") { - seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; - return Response.json({ number: prNumber, state: "closed" }); - } - if (url.endsWith(`/pulls/${prNumber}`)) return Response.json({ number: prNumber, state: "open", head: { sha: `sha${prNumber}` }, mergeable_state: "clean" }); - if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - // Repo-level label definition (createMissingLabel: true probes/creates the label before applying it). - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); - if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { - seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); - return Response.json({ id: seen.comments.length }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - } - - it("is off by default — no ping is tracked and no cooldown action fires", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 200, title: "Off by default", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" }); - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(200, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-off-default", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 200, title: "Off by default", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(0); - expect(seen.closed).toBe(false); - }); - - it("REGRESSION (gate-flagged): caps an oversized review-nag cooldown at MAX_REVIEW_NAG_COOLDOWN_DAYS before Date arithmetic, even when the resolved settings object itself carries an oversized value", async () => { - // upsertRepositorySettings/getRepositorySettings both clamp reviewNagCooldownDays on write AND read, so - // seeding an oversized value through the normal repository layer (even via a raw DB update bypassing the - // write-time clamp) can never actually reach maybeThrottleReviewNagPing uncapped -- the read-time clamp in - // 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 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 - // count; if the guard were removed, the uncapped window would count all three, crossing maxPings=3. - vi.setSystemTime(new Date("2025-04-24T00:00:00.000Z")); - for (let i = 0; i < 3; i += 1) { - await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#206", outcome: "completed" }); - } - vi.setSystemTime(new Date("2026-05-29T00:00:00.000Z")); // ~400 days later - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - const resolveSettingsSpy = vi - .spyOn(repositorySettingsModule, "resolveRepositorySettings") - .mockResolvedValueOnce({ ...baseSettings, reviewNagCooldownDays: 1_000_000_000 }); - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(206, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-huge-cooldown", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 206, title: "Huge cooldown", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - - // The 400-day-old pings fell outside the CAPPED 365-day window, so this is only the 1st ping this - // window — under maxPings=3, never throttled. An uncapped window would have counted all 3 prior pings - // (pingCount=4 > maxPings=3) and applied the cooldown instead. - const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); - expect(applied?.n).toBe(0); - expect(seen.closed).toBe(false); - expect(seen.comments.some((c) => c.includes("cooldown limit"))).toBe(false); - expect(resolveSettingsSpy).toHaveBeenCalled(); - resolveSettingsSpy.mockRestore(); - }); - - it("records pings under the configured threshold without acting; the normal @gittensory reply still proceeds", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", 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); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-under-threshold", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 201, title: "Under threshold", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(1); // the ping is recorded (1st of 3 allowed) - const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); - expect(applied?.n).toBe(0); // but no cooldown action — under threshold - expect(seen.closed).toBe(false); - // The review-nag hook returned false (fell through) — proven by the NORMAL mention-command dispatch - // making its own (here: unauthorized-skip) decision, rather than review-nag's short-circuit ever firing. - const skipped = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.agent_command_skipped'").first<{ n: number }>(); - expect(skipped?.n).toBeGreaterThanOrEqual(1); - }); - - 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(202, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-hold", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 202, title: "Hold cooldown", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); - expect(seen.comments.some((c) => c.includes("cooldown limit"))).toBe(true); - // Only ONE comment posted — the short-circuit skipped the normal answer-card dispatch. - expect(seen.comments).toHaveLength(1); - const applied = await env.DB.prepare("select outcome, detail from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ outcome: string; detail: string }>(); - expect(applied?.outcome).toBe("completed"); - expect(applied?.detail).toContain("hold applied"); - }); - - it("close policy on a PR thread: labels + closes once the threshold is crossed, with no merit review", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(203, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-close", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 203, title: "Close cooldown", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(true); - expect(seen.labels).toContain("too-chatty"); // configurable label, not hardcoded - expect(seen.comments.some((c) => c.includes("chatty") && c.includes("4 times"))).toBe(true); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - }); - - it("REGRESSION (#review-nag-cross-pr-carryover): a contributor who exhausted their pings on PR A carries the count over to a BRAND-NEW PR B instead of resetting to a clean 0/maxPings slate", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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" } }); - // 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: "" }); - for (let i = 0; i < 3; i += 1) { - await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "JSONbored/gittensory#220", outcome: "completed" }); - } - // PR B: a BRAND-NEW PR from the SAME contributor -- a new issue.number means a new targetKey the old - // per-target count would treat as a clean slate. Only ONE ping is sent here. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 221, title: "PR B (brand new)", state: "open", user: { login: "chatty" }, head: { sha: "sha221" }, author_association: "NONE", labels: [], body: "" }); - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(221, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-carryover-pr-b", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 221, title: "PR B (brand new)", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - // Under the OLD per-targetKey count, this is ping 1/3 on PR B alone -- under threshold, no action. The - // FIX counts every prior ping across the whole repo, so this single PR-B ping is already #4 overall - // (3 carried over from PR A + this one), crossing maxPings=3 on the very first PR-B ping. - expect(seen.closed).toBe(true); - expect(seen.comments.some((c) => c.includes("chatty") && c.includes("4 times"))).toBe(true); - const prA = await env.DB.prepare("select state from pull_requests where number = 220").first<{ state: string }>(); - expect(prA?.state).toBe("closed"); // PR A is untouched by this second evaluation - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - }); - - 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 }); - 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(204, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-issue-degrade", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 204, title: "Plain issue", state: "open", user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); // no closeIssue primitive — degrades to hold - expect(seen.comments.some((c) => c.includes("cooldown limit"))).toBe(true); - }); - - 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(205, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-exempt", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 205, title: "Exempt author", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); - const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); - expect(applied?.n).toBe(0); - }); - - 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 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); - for (let i = 0; i < 5; i += 1) { - await processJob(env, { - type: "github-webhook", - deliveryId: `nag-third-party-${i}`, - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 206, title: "Third party pinger", state: "open", pull_request: {}, user: { login: "pr-author" }, author_association: "NONE" }, - comment: { id: i, body: "@gittensory help", user: { login: "bystander", type: "User" }, author_association: "NONE" }, - }, - }); - } - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(0); // never even tracked — the commenter is not the thread's own author - expect(seen.closed).toBe(false); - }); - - it("no-op owner-exemption when repoFullName has no slash (repoOwner is empty — never wrongly matches the commenter)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - 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 }); - 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(209, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-noslash", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }, - issue: { number: 209, title: "Slash-free repo", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - // repoOwner="" (branch false) → commenter "chatty" never equals "" → the owner-exemption is skipped and - // the throttle still engages normally (the comment post itself can't succeed for a slash-free repo — no - // owner/repo to target — but that failure is swallowed by design, same as every other best-effort notice - // in this file). Proven by reaching + completing the hold branch without the handler crashing. - const applied = await env.DB.prepare("select outcome from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ outcome: string }>(); - expect(applied?.outcome).toBe("completed"); - }); - - 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 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); - for (let i = 0; i < 3; i += 1) { - await processJob(env, { - type: "github-webhook", - deliveryId: `nag-owner-${i}`, - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 207, title: "Owner PR", state: "open", pull_request: {}, user: { login: "JSONbored" }, author_association: "OWNER" }, - comment: { id: i, body: "@gittensory help", user: { login: "JSONbored", type: "User" }, author_association: "OWNER" }, - }, - }); - } - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(0); - expect(seen.closed).toBe(false); - }); - - 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(208, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-admin", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 208, title: "Admin PR", state: "open", pull_request: {}, user: { login: "fleet-admin" }, author_association: "NONE" }, - comment: { id: 6, body: "@gittensory help", user: { login: "fleet-admin", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); - const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); - expect(applied?.n).toBe(0); - }); - - 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(209, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-hold-dryrun", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 209, title: "Dry-run hold", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.comments).toHaveLength(0); // dry-run — no live comment posted - const applied = await env.DB.prepare("select outcome from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ outcome: string }>(); - expect(applied?.outcome).toBe("denied"); - }); - - 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(210, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-already-closed", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 210, title: "Already closed", state: "closed", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); - const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ n: number }>(); - expect(applied?.n).toBe(0); // fell through silently — nothing left to act on - }); - - 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(211, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-observe-only", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 211, title: "Observe-only autonomy", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); - const applied = await env.DB.prepare("select outcome, detail from audit_events where event_type = 'github_app.review_nag_cooldown_applied'").first<{ outcome: string; detail: string }>(); - expect(applied?.outcome).toBe("denied"); - expect(applied?.detail).toContain("autonomy is not acting"); - }); - - 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubReviewNagFetch(212, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "nag-no-installation", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 212, title: "No installation row", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 4, body: "@gittensory help", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); // no installation permissions on record — the write-permission gate denies it - const closeAudit = await env.DB.prepare("select outcome from audit_events where event_type = 'agent.action.close'").first<{ outcome: string }>(); - expect(closeAudit?.outcome).toBe("denied"); - }); - }); - - describe("maintainer-mention nag moderation (#label-scoping)", () => { - function stubMonitoredMentionFetch(prNumber: number, seen: { comments: string[]; labels: string[]; closed: boolean }) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "none" }); - if (url.endsWith(`/pulls/${prNumber}`) && method === "PATCH") { - seen.closed = JSON.parse(String(init?.body ?? "{}")).state === "closed"; - return Response.json({ number: prNumber, state: "closed" }); - } - if (url.endsWith(`/pulls/${prNumber}`)) return Response.json({ number: prNumber, state: "open", head: { sha: `sha${prNumber}` }, mergeable_state: "clean" }); - if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { - seen.labels.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); - if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { - seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); - return Response.json({ id: seen.comments.length }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - } - - 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 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); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-off-default", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 300, title: "No monitored logins", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@JSONbored are you going to review this?", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(0); - }); - - 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 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); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-under-threshold", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 301, title: "Under threshold", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "Hey @JSONbored can you take a look?", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(1); - expect(seen.closed).toBe(false); - }); - - 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 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); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-bot-shaped-literal", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 312, title: "Bot mention", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "Please check this @dependabot[bot].", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(1); - }); - - 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 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); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-bot-shaped-false-positive", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 313, title: "Bot false positive", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "This mentions @dependabotb, not the bot actor.", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(0); - }); - - 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 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); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-case-insensitive", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 302, title: "Case + unrelated", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@jsonbored please review", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(1); // case-insensitive match on the configured "JSONbored" - - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-unrelated", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 302, title: "Case + unrelated", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 2, body: "this uses @some-other-package internally", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const pingsAfter = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(pingsAfter?.n).toBe(1); // unrelated mention did not add a ping - }); - - it("counts a monitored-login mention independently of the @gittensory 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 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); - // A comment mentioning BOTH @gittensory and the monitored login should tick both counters independently. - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-both", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 303, title: "Independent counters", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@gittensory help — also @JSONbored can you look?", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - const gittensoryPings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>(); - const mentionPings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(gittensoryPings?.n).toBe(1); - expect(mentionPings?.n).toBe(1); - }); - - 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubMonitoredMentionFetch(304, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-hold", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 304, title: "Hold on mention", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@JSONbored please look at this", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); - expect(seen.comments.some((c) => c.includes("cooldown limit for @JSONbored"))).toBe(true); - expect(seen.comments).toHaveLength(1); // short-circuited — no normal answer-card reply - }); - - it("close policy on a PR thread: labels + closes once the threshold is crossed, reusing reviewNagLabel", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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 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" }); - } - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubMonitoredMentionFetch(305, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-close", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 305, title: "Close on mention", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@JSONbored please look at this", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(true); - expect(seen.labels).toContain("too-chatty"); - // #label-scoping: close: "auto" alone (no broad label: "auto") is sufficient for the label AND the close. - }); - - it("REGRESSION (#review-nag-cross-pr-carryover): a contributor who exhausted their @-mention pings for ONE login on PR A carries that login's count over to a BRAND-NEW PR B", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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" } }); - // 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) { - await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.monitored_mention_ping", actor: "chatty", targetKey: "JSONbored/gittensory#320#mention:jsonbored", outcome: "completed" }); - } - // PR B: a BRAND-NEW PR from the SAME contributor mentioning the SAME login. A new issue.number is a new - // targetKey the old per-target count would treat as a clean slate. Only ONE mention is sent here. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 321, title: "PR B (brand new)", state: "open", user: { login: "chatty" }, head: { sha: "sha321" }, author_association: "NONE", labels: [], body: "" }); - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubMonitoredMentionFetch(321, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-carryover-pr-b", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 321, title: "PR B (brand new)", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@JSONbored please look at this", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - // Under the OLD per-targetKey count, this is mention-ping 1/3 on PR B alone -- under threshold, no action. - // The FIX counts every prior @JSONbored mention-ping across the whole repo, so this single PR-B ping is - // already #4 overall (3 carried over from PR A + this one), crossing maxPings=3 on the very first ping. - expect(seen.closed).toBe(true); - const prA = await env.DB.prepare("select state from pull_requests where number = 320").first<{ state: string }>(); - expect(prA?.state).toBe("closed"); // PR A is untouched by this second evaluation - }); - - it("REGRESSION (#review-nag-cross-pr-carryover): a DIFFERENT monitored login mentioned on PR B keeps its own independent budget, unaffected by another login's exhausted count", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: { metadata: "read", 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" }, - }); - // 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) { - await repositoriesModule.recordAuditEvent(env, { eventType: "github_app.monitored_mention_ping", actor: "chatty", targetKey: "JSONbored/gittensory#322#mention:jsonbored", outcome: "completed" }); - } - // PR B: the SAME contributor mentions a DIFFERENT monitored login ("other-maintainer") for the FIRST time. - // If the repo-wide carryover fix accidentally merged every mentioned login into one shared count, this - // single ping would incorrectly already be "#4" and get throttled -- it must instead be a fresh 1/3. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 323, title: "PR B (different login)", state: "open", user: { login: "chatty" }, head: { sha: "sha323" }, author_association: "NONE", labels: [], body: "" }); - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubMonitoredMentionFetch(323, seen); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-independent-login-pr-b", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 323, title: "PR B (different login)", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@other-maintainer could you take a look?", user: { login: "chatty", type: "User" }, author_association: "NONE" }, - }, - }); - expect(seen.closed).toBe(false); // "other-maintainer"'s own budget is untouched by @JSONbored's exhausted count - const mentionPings = await env.DB.prepare( - "select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping' and target_key = 'JSONbored/gittensory#323#mention:other-maintainer'", - ).first<{ n: number }>(); - expect(mentionPings?.n).toBe(1); // recorded as ping 1/3 for THIS login, not folded into @JSONbored's tally - }); - - 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"] }); - const seen = { comments: [] as string[], labels: [] as string[], closed: false }; - stubMonitoredMentionFetch(306, seen); - for (const [commenter, prNumber] of [ - ["JSONbored", 306], // repo owner - ["fleet-admin", 307], // admin login - ["some-bot[bot]", 308], // automation bot - ["trusted-regular", 309], // configured exemption - ] as const) { - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: prNumber, title: "Exempt", state: "open", user: { login: commenter }, author_association: "NONE", labels: [], body: "" }); - await processJob(env, { - type: "github-webhook", - deliveryId: `mention-exempt-${prNumber}`, - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: prNumber, title: "Exempt", state: "open", pull_request: {}, user: { login: commenter }, author_association: "NONE" }, - comment: { id: prNumber, body: "@JSONbored can you review?", user: { login: commenter, type: commenter.endsWith("[bot]") ? "Bot" : "User" }, author_association: "NONE" }, - }, - }); - } - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(0); - }); - - 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 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); - await processJob(env, { - type: "github-webhook", - deliveryId: "mention-third-party", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 310, title: "Third party", state: "open", pull_request: {}, user: { login: "thread-author" }, author_association: "NONE" }, - comment: { id: 1, body: "@JSONbored can you weigh in here?", user: { login: "a-different-commenter", type: "User" }, author_association: "NONE" }, - }, - }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - expect(pings?.n).toBe(0); - }); - - 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 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); - const payload = { - action: "created" as const, - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" as const } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 311, title: "Redelivery", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" }, - comment: { id: 1, body: "@JSONbored ping", user: { login: "chatty", type: "User" as const }, author_association: "NONE" }, - }; - await processJob(env, { type: "github-webhook", deliveryId: "mention-redelivery-same", eventName: "issue_comment", payload }); - await processJob(env, { type: "github-webhook", deliveryId: "mention-redelivery-same", eventName: "issue_comment", payload }); - const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>(); - // NOTE: unlike #2560's per-command limiter, review-nag/monitored-mention ping recording does not itself - // dedup by deliveryId -- it always records. This assertion documents CURRENT behavior (2 pings from 2 - // deliveries) rather than asserting an idempotency guarantee this handler does not provide. - expect(pings?.n).toBe(2); - }); - }); - - describe("per-command @gittensory rate limit (#2560)", () => { - function stubCommandRateLimitFetch(issueNumber: number, seen: { comments: string[] }) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); - if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "maintain" }); - if (url.includes(`/issues/${issueNumber}/comments`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${issueNumber}/comments`) && method === "POST") { - seen.comments.push(String(JSON.parse(String(init?.body ?? "{}")).body ?? "")); - return Response.json({ id: seen.comments.length }, { status: 201 }); - } - return new Response("not found", { status: 404 }); - }); - } - - function mentionPayload(issueNumber: number, body: string) { - return { - action: "created" as const, - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: issueNumber, title: "Rate limit target", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { id: 1, body, user: { login: "maintainer", type: "User" }, author_association: "OWNER" }, - }; - } - - it("is off by default — no invocation is tracked and every command dispatches normally", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 300, title: "Rate limit target", state: "open", user: { login: "oktofeesh1" }, author_association: "NONE", labels: [], body: "" }); - const seen = { comments: [] as string[] }; - stubCommandRateLimitFetch(300, seen); - for (let i = 0; i < 25; i += 1) { - await processJob(env, { type: "github-webhook", deliveryId: `rl-off-${i}`, eventName: "issue_comment", payload: mentionPayload(300, "@gittensory help") }); - } - const invocations = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_invocation'").first<{ n: number }>(); - expect(invocations?.n).toBe(0); - expect(seen.comments).toHaveLength(25); // every one of the 25 invocations dispatched normally - }); - - 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 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); - await processJob(env, { type: "github-webhook", deliveryId: "rl-under", eventName: "issue_comment", payload: mentionPayload(301, "@gittensory help") }); - const invocations = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_invocation'").first<{ n: number }>(); - expect(invocations?.n).toBe(1); // 1st of 5 allowed - const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ n: number }>(); - expect(applied?.n).toBe(0); // under threshold — no hold - expect(seen.comments).toHaveLength(1); // the normal answer card still posted - }); - - 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 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" }); - } - const seen = { comments: [] as string[] }; - stubCommandRateLimitFetch(302, seen); - await processJob(env, { type: "github-webhook", deliveryId: "rl-cheap-over", eventName: "issue_comment", payload: mentionPayload(302, "@gittensory help") }); - // Only ONE comment posted — the short-circuit skipped the normal answer-card dispatch. - expect(seen.comments).toHaveLength(1); - expect(seen.comments[0]).toContain("rate limit"); - const applied = await env.DB.prepare("select outcome, detail from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ outcome: string; detail: string }>(); - expect(applied?.outcome).toBe("completed"); - expect(applied?.detail).toContain("hold applied"); - }); - - 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 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" }); - } - const seen = { comments: [] as string[] }; - stubCommandRateLimitFetch(303, seen); - await processJob(env, { type: "github-webhook", deliveryId: "rl-ai-over", eventName: "issue_comment", payload: mentionPayload(303, "@gittensory next-action") }); - expect(seen.comments).toHaveLength(1); - expect(seen.comments[0]).toContain("rate limit"); - const applied = await env.DB.prepare("select detail from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ detail: string }>(); - expect(applied?.detail).toContain("limit 2"); // the AI limit (2), not the cheap default (20) - }); - - 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 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" }); - const seen = { comments: [] as string[] }; - stubCommandRateLimitFetch(304, seen); - // A DIFFERENT command ("miner-context") on the same thread by the same actor must not be affected. - await processJob(env, { type: "github-webhook", deliveryId: "rl-independent", eventName: "issue_comment", payload: mentionPayload(304, "@gittensory miner-context") }); - expect(seen.comments).toHaveLength(1); - expect(seen.comments[0]).not.toContain("rate limit"); - const applied = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ n: number }>(); - expect(applied?.n).toBe(0); - }); - - 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 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[] }; - stubCommandRateLimitFetch(305, seen); - await processJob(env, { type: "github-webhook", deliveryId: "rl-dry-run", eventName: "issue_comment", payload: mentionPayload(305, "@gittensory help") }); - expect(seen.comments).toHaveLength(0); // held, but dry-run posts nothing live - const applied = await env.DB.prepare("select outcome from audit_events where event_type = 'github_app.command_rate_limit_applied'").first<{ outcome: string }>(); - expect(applied?.outcome).toBe("denied"); - }); - - it("REGRESSION: a redelivered webhook (same deliveryId) does not double-count — the replay is a no-op, not a second invocation", async () => { - // GitHub can and does redeliver the same issue_comment event (timeout/retry). Before the fix, the - // 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 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); - // The SAME deliveryId, redelivered — GitHub's own retry behavior on a timeout/5xx. - await processJob(env, { type: "github-webhook", deliveryId: "rl-redelivered", eventName: "issue_comment", payload: mentionPayload(306, "@gittensory help") }); - await processJob(env, { type: "github-webhook", deliveryId: "rl-redelivered", eventName: "issue_comment", payload: mentionPayload(306, "@gittensory help") }); - - const invocations = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_invocation'").first<{ n: number }>(); - expect(invocations?.n).toBe(1); // only ONE invocation recorded despite two processing passes - expect(seen.comments).toHaveLength(1); // the replay is suppressed entirely — no second answer card - expect(seen.comments.every((c) => !c.includes("rate limit"))).toBe(true); - const suppressed = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.command_redelivery_suppressed'").first<{ n: number }>(); - expect(suppressed?.n).toBe(1); - }); - }); - - it("denies a maintainer Q&A command from an org member without real repo permission (#788)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 96, - title: "Org member tries a maintainer command", - state: "open", - user: { login: "alice" }, - author_association: "NONE", - labels: [], - body: "", - }); - const calls = { comments: 0, permission: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // The commenter is an org MEMBER but has only READ access to THIS repo — not a maintainer/collaborator. - if (url.includes("/collaborators/orgmember/permission")) { - calls.permission += 1; - return Response.json({ permission: "read" }); - } - if (url.includes("/issues/") && url.includes("/comments")) { - calls.comments += 1; - return Response.json([]); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-org-member-no-permission", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 96, title: "Org member tries a maintainer command", state: "open", pull_request: {}, user: { login: "alice" }, author_association: "NONE" }, - // author_association MEMBER would have granted the maintainer role pre-#788; it no longer does. - comment: { id: 96, body: "@gittensory queue-summary", user: { login: "orgmember", type: "User" }, author_association: "MEMBER" }, - }, - }); - expect(calls.permission).toBe(1); // the REAL repo permission was consulted, not the spoofable association - expect(calls.comments).toBe(0); // …and the org member was denied — no maintainer reply - const skip = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") - .bind("github_app.agent_command_skipped", "JSONbored/gittensory#96") - .first<{ detail: string }>(); - expect(skip?.detail).toBe("not_maintainer_or_pr_author"); - }); - - it("records command usage as an error when miner authorization cannot be checked", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return new Response("api down", { status: 503 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-miner-unavailable", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 84, title: "Miner unavailable PR", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { id: 5, body: "@gittensory preflight", user: { login: "oktofeesh1", type: "User" }, author_association: "NONE" }, - }, - }); - - const usageEvents = await listProductUsageEvents(env, { limit: 5 }); - expect(usageEvents).toEqual([ - expect.objectContaining({ surface: "github_app", eventName: "agent_command_skipped", outcome: "error", metadata: expect.objectContaining({ reason: "miner_detection_unavailable" }) }), - ]); - }); - - it("does not let product usage write failures block GitHub command audits", async () => { - const env = withProductUsageInsertFailure(createTestEnv()); - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-product-usage-down", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 90, title: "Plain issue", state: "open", user: { login: "reporter" } }, - comment: { id: 1, body: "@gittensory preflight", user: { login: "reporter", type: "User" }, author_association: "NONE" }, - }, - }); - - const audit = await env.DB.prepare("select event_type, detail from audit_events where target_key = ?") - .bind("JSONbored/gittensory#90") - .all<{ event_type: string; detail: string }>(); - expect(audit.results).toEqual([expect.objectContaining({ event_type: "github_app.agent_command_skipped", detail: "not_a_pull_request_thread" })]); - }); - - it("audits command authorization errors when miner detection is unavailable", async () => { - const env = createTestEnv(); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString() === "https://api.gittensor.io/miners") return new Response("unavailable", { status: 503 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "agent-command-miner-unavailable", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 84, title: "Unavailable miner check", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, - comment: { id: 5, body: "@gittensory preflight", user: { login: "oktofeesh1", type: "User" }, author_association: "NONE" }, - }, - }); - - const event = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?") - .bind("github_app.agent_command_skipped", "JSONbored/gittensory#84") - .first<{ outcome: string; detail: string }>(); - expect(event).toMatchObject({ outcome: "error", detail: "miner_detection_unavailable" }); - }); - - it("detects a changes-requested review notification for the PR author", async () => { - const enqueued: Array<{ type: string }> = []; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - JOBS: { - async send(message: { type: string }) { - enqueued.push(message); - }, - } as unknown as Queue, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/repos/JSONbored/gittensory/collaborators/maintainer/permission")) return Response.json({ permission: "maintain" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-changes-requested", - eventName: "pull_request_review", - payload: { - action: "submitted", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 42, - title: "Add feature", - state: "open", - user: { login: "contributor", type: "User" }, - html_url: "https://github.com/JSONbored/gittensory/pull/42", - }, - review: { - state: "changes_requested", - user: { login: "maintainer", type: "User" }, - submitted_at: "2026-05-28T12:00:00.000Z", - html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", - }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - const detected = await env.DB.prepare("select actor, target_key, outcome, detail, metadata_json from audit_events where event_type = ?") - .bind("notification.event_detected") - .all<{ actor: string; target_key: string; outcome: string; detail: string; metadata_json: string }>(); - expect(detected.results).toHaveLength(1); - expect(detected.results[0]).toMatchObject({ - actor: "maintainer", - target_key: "contributor", - outcome: "success", - detail: "pull_request_changes_requested for JSONbored/gittensory#42", - }); - expect(JSON.parse(detected.results[0]!.metadata_json)).toMatchObject({ - deliveryId: "review-changes-requested", - eventType: "pull_request_changes_requested", - recipientLogin: "contributor", - repoFullName: "JSONbored/gittensory", - pullNumber: 42, - dedupKey: "changes_requested:JSONbored/gittensory#42:maintainer:2026-05-28T12:00:00.000Z", - }); - expect(JSON.stringify(detected.results[0])).not.toMatch(/trust score|wallet|hotkey|reward estimate|reviewability/i); - - const evaluateJob = enqueued.find((message): message is { type: "notify-evaluate"; events: Array<{ recipientLogin: string }> } => message.type === "notify-evaluate"); - expect(evaluateJob).toBeDefined(); - expect(evaluateJob!.events).toHaveLength(1); - expect(evaluateJob!.events[0]!.recipientLogin).toBe("contributor"); - }); - - it("skips changes-requested review notifications from reviewers without repository write permission", async () => { - const enqueued: Array<{ type: string }> = []; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - JOBS: { - async send(message: { type: string }) { - enqueued.push(message); - }, - } as unknown as Queue, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/repos/JSONbored/gittensory/collaborators/drive-by-user/permission")) return Response.json({ permission: "read" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-changes-requested-low-priv", - eventName: "pull_request_review", - payload: { - action: "submitted", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 42, - title: "Add feature", - state: "open", - user: { login: "contributor", type: "User" }, - html_url: "https://github.com/JSONbored/gittensory/pull/42", - }, - review: { - state: "changes_requested", - user: { login: "drive-by-user", type: "User" }, - submitted_at: "2026-05-28T12:00:00.000Z", - html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", - }, - sender: { login: "drive-by-user", type: "User" }, - }, - }); - - const detected = await env.DB.prepare("select actor from audit_events where event_type = ?") - .bind("notification.event_detected") - .all<{ actor: string }>(); - expect(detected.results).toEqual([]); - expect(enqueued).not.toContainEqual(expect.objectContaining({ type: "notify-evaluate" })); - }); - - it("skips changes-requested review notifications with an unknown actor without consulting repo permissions", async () => { - const enqueued: Array<{ type: string }> = []; - const permissionCalls: string[] = []; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - JOBS: { - async send(message: { type: string }) { - enqueued.push(message); - }, - } as unknown as Queue, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/")) { - permissionCalls.push(url); - return Response.json({ permission: "admin" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-changes-requested-unknown-actor", - eventName: "pull_request_review", - payload: { - action: "submitted", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 42, - title: "Add feature", - state: "open", - user: { login: "contributor", type: "User" }, - html_url: "https://github.com/JSONbored/gittensory/pull/42", - }, - // Neither the review nor the sender carries a login → detectNotificationEvents emits actorLogin "unknown". - review: { - state: "changes_requested", - submitted_at: "2026-05-28T12:00:00.000Z", - html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", - }, - }, - }); - - const detected = await env.DB.prepare("select actor from audit_events where event_type = ?") - .bind("notification.event_detected") - .all<{ actor: string }>(); - expect(detected.results).toEqual([]); - expect(enqueued).not.toContainEqual(expect.objectContaining({ type: "notify-evaluate" })); - // The unknown-actor guard short-circuits before any collaborator-permission lookup. - expect(permissionCalls).toEqual([]); - }); - - it("skips changes-requested review notifications when the webhook has no installation", async () => { - const enqueued: Array<{ type: string }> = []; - const permissionCalls: string[] = []; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - JOBS: { - async send(message: { type: string }) { - enqueued.push(message); - }, - } as unknown as Queue, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/")) { - permissionCalls.push(url); - return Response.json({ permission: "admin" }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-changes-requested-no-installation", - eventName: "pull_request_review", - payload: { - action: "submitted", - // No installation present → installationId is undefined and the reviewer cannot be verified. - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 42, - title: "Add feature", - state: "open", - user: { login: "contributor", type: "User" }, - html_url: "https://github.com/JSONbored/gittensory/pull/42", - }, - review: { - state: "changes_requested", - user: { login: "maintainer", type: "User" }, - submitted_at: "2026-05-28T12:00:00.000Z", - html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", - }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - const detected = await env.DB.prepare("select actor from audit_events where event_type = ?") - .bind("notification.event_detected") - .all<{ actor: string }>(); - expect(detected.results).toEqual([]); - expect(enqueued).not.toContainEqual(expect.objectContaining({ type: "notify-evaluate" })); - // With no installation we cannot verify the reviewer, so no permission lookup is attempted. - expect(permissionCalls).toEqual([]); - }); - - it.each(["submitted", "dismissed", "edited"] as const)( - "bumps reviewsInvalidatedAt for the right repo+PR on a pull_request_review '%s' webhook (#2537)", - async (action) => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - JOBS: { async send() {} } as unknown as Queue, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - // Seed an existing sync-state row so the assertion can confirm ONLY reviewsInvalidatedAt moved. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 42, - title: "Add feature", - state: "open", - user: { login: "contributor" }, - head: { sha: "sha-42" }, - labels: [], - body: "", - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: `review-invalidate-${action}`, - eventName: "pull_request_review", - payload: { - action, - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 42, - title: "Add feature", - state: "open", - user: { login: "contributor", type: "User" }, - html_url: "https://github.com/JSONbored/gittensory/pull/42", - }, - review: { - state: action === "dismissed" ? "DISMISSED" : "APPROVED", - user: { login: "maintainer", type: "User" }, - submitted_at: "2026-05-28T12:00:00.000Z", - html_url: "https://github.com/JSONbored/gittensory/pull/42#pullrequestreview-1", - }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - const state = await getPullRequestDetailSyncState(env, "JSONbored/gittensory", 42); - expect(state?.reviewsInvalidatedAt).toBeTruthy(); - }, - ); - - it("does not bump reviewsInvalidatedAt for a pull_request_review action outside submitted/dismissed/edited", async () => { - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - JOBS: { async send() {} } as unknown as Queue, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-invalidate-unsupported-action", - eventName: "pull_request_review", - payload: { - // "submitted" | "dismissed" | "edited" are the only invalidating actions; GitHub also emits others - // (e.g. review comments carry their own event) that must NOT stamp the cache marker. - action: "unrecognized_action" as unknown as "submitted", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 43, - title: "Add feature", - state: "open", - user: { login: "contributor", type: "User" }, - html_url: "https://github.com/JSONbored/gittensory/pull/43", - }, - review: { - state: "APPROVED", - user: { login: "maintainer", type: "User" }, - submitted_at: "2026-05-28T12:00:00.000Z", - html_url: "https://github.com/JSONbored/gittensory/pull/43#pullrequestreview-1", - }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - expect(await getPullRequestDetailSyncState(env, "JSONbored/gittensory", 43)).toBeNull(); - }); - - it("notifies issue-watchers when a new grabbable maintainer-created issue opens (#699 path B)", async () => { - const enqueued: Array<{ type: string; events?: Array<{ eventType: string; recipientLogin: string; pullNumber: number }> }> = []; - const env = createTestEnv({ JOBS: { async send(message: { type: string }) { enqueued.push(message); } } as unknown as Queue }); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest - const watcherLogins = Array.from({ length: 205 }, (_, index) => `watcher-${String(index + 1).padStart(3, "0")}`); - for (const login of watcherLogins) { - await upsertIssueWatchSubscription(env, { login, repoFullName: "JSONbored/gittensory" }); - } - await upsertIssueWatchSubscription(env, { login: "maintainer", repoFullName: "JSONbored/gittensory" }); // the author — should be skipped - - await processJob(env, { - type: "github-webhook", - deliveryId: "issue-watch-open", - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 91, title: "Add caching to the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }, - }, - }); - - // Batched but bounded (#selfhost-maintenance-self-pin): watcher matches from this ONE webhook delivery ride in - // chunked notify-evaluate jobs, not one job per watcher and not one unbounded queue payload. - const evaluateJobs = enqueued.filter((m): m is { type: "notify-evaluate"; events: Array<{ eventType: string; recipientLogin: string; pullNumber: number }> } => m.type === "notify-evaluate"); - expect(evaluateJobs.map((job) => job.events).map((events) => events.length)).toEqual([100, 100, 5]); - const watchEvents = evaluateJobs.flatMap((job) => job.events).filter((event) => event.eventType === "issue_watch_match"); - expect(watchEvents.map((event) => event.recipientLogin).sort()).toEqual(watcherLogins); // maintainer (author) skipped - expect(watchEvents.every((event) => event.pullNumber === 91)).toBe(true); - - const detected = await env.DB.prepare("select metadata_json from audit_events where event_type = 'notification.event_detected' and target_key = ?").bind("watcher-001").first<{ metadata_json: string }>(); - expect(JSON.parse(detected!.metadata_json)).toMatchObject({ eventType: "issue_watch_match", recipientLogin: "watcher-001", repoFullName: "JSONbored/gittensory" }); - }); - - it("REGRESSION (#3218 review): chunk membership across a >100-watcher batch is order-independent -- the SAME watcher set in a different arrival order still produces the SAME set of chunk coalesce keys", async () => { - const watcherLogins = Array.from({ length: 205 }, (_, index) => `watcher-${String(index + 1).padStart(3, "0")}`); - - const enqueueNotifyEvaluateJobs = async (loginOrder: string[]): Promise }>> => { - const enqueued: Array<{ type: string; events?: Array<{ dedupKey: string }> }> = []; - const env = createTestEnv({ JOBS: { async send(message: { type: string }) { enqueued.push(message); } } as unknown as Queue }); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest - // listIssueWatchersForRepo has no ORDER BY -- insertion order IS read-back order, so inserting in a - // different order here genuinely reproduces two logically-identical detection passes disagreeing on - // notificationEvents' arrival order, exactly the redelivery scenario the review is concerned about. - for (const login of loginOrder) { - await upsertIssueWatchSubscription(env, { login, repoFullName: "JSONbored/gittensory" }); - } - await processJob(env, { - type: "github-webhook", - deliveryId: `issue-watch-open-${loginOrder[0]}`, - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 91, title: "Add caching to the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }, - }, - }); - vi.unstubAllGlobals(); - return enqueued.filter((m): m is { type: "notify-evaluate"; events: Array<{ dedupKey: string }> } => m.type === "notify-evaluate"); - }; - - const coalesceKeysFor = (jobs: Array<{ type: string; events: Array<{ dedupKey: string }> }>): Array => - jobs.map((job) => jobCoalesceKey(JSON.stringify(job))).sort(); - - const forwardJobs = await enqueueNotifyEvaluateJobs(watcherLogins); - const reversedJobs = await enqueueNotifyEvaluateJobs([...watcherLogins].reverse()); - - // Same chunk SIZES either way (chunking itself is unaffected -- only membership was the risk). - expect(forwardJobs.map((job) => job.events.length)).toEqual([100, 100, 5]); - expect(reversedJobs.map((job) => job.events.length)).toEqual([100, 100, 5]); - // The set of chunk-level coalesce keys must match -- proving a redelivery whose events resolve in a - // different order still coalesces with the original batch instead of silently re-running as "new" work. - expect(coalesceKeysFor(reversedJobs)).toEqual(coalesceKeysFor(forwardJobs)); - }); - - it("appends issue-side slop findings to the issue advisory only when slop is opted in (#533)", async () => { - const env = createTestEnv(); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); // no .gittensory.yml → empty manifest - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositoryFromGitHub(env, { name: "other", full_name: "JSONbored/other", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", slopGateMode: "advisory" }); - // JSONbored/other keeps the default slopGateMode "off". - - const emptyBodyIssue = (repoFull: string, name: string, number: number) => ({ - type: "github-webhook" as const, - deliveryId: `issue-slop-${number}`, - eventName: "issues", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name, full_name: repoFull, private: false, owner: { login: "JSONbored" } }, - issue: { number, title: "Something is broken", state: "open", user: { login: "reporter" }, body: " " }, - }, - }); - await processJob(env, emptyBodyIssue("JSONbored/gittensory", "gittensory", 501)); - await processJob(env, emptyBodyIssue("JSONbored/other", "other", 502)); - - const slopOn = await env.DB.prepare("select findings_json from advisories where target_type = 'issue' and repo_full_name = ?").bind("JSONbored/gittensory").first<{ findings_json: string }>(); - const slopOff = await env.DB.prepare("select findings_json from advisories where target_type = 'issue' and repo_full_name = ?").bind("JSONbored/other").first<{ findings_json: string }>(); - expect(slopOn?.findings_json).toContain("empty_issue_body"); // opted in → triage finding present - expect(slopOff?.findings_json ?? "").not.toContain("empty_issue_body"); // default off → no slop finding - }); - - it("clears the persisted dashboard slop score when the slop gate is off (#911)", async () => { - // Merge-readiness still collects the live slop score, so shouldCollectSlopEvidence runs even with the - // slop gate disabled — but with slopGateMode "off" the persisted dashboard row must be cleared to null - // so a previously cached score doesn't linger after a maintainer disables slop. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - slopGateMode: "off", // dashboard slop disabled… - mergeReadinessGateMode: "advisory", // …but readiness keeps the live score in play - }); - // Seed the PR row plus a stale dashboard slop score that the slop-off pass must clear. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 91, - title: "Add helper", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "slopoff123" }, - labels: [], - body: "Adds a helper.", - }); - await updatePullRequestSlopAssessment(env, "JSONbored/gittensory", 91, { slopRisk: 80, slopBand: "high" }); - await upsertPullRequestFile(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 91, - path: "src/helper.ts", - status: "modified", - additions: 5, - deletions: 0, - changes: 5, - payload: {}, - }); - expect((await getPullRequest(env, "JSONbored/gittensory", 91))?.slopRisk).toBe(80); // stale score present pre-run - - const refreshedFiles: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/91/files")) { - refreshedFiles.push(url); - return Response.json([{ filename: "src/helper.ts", status: "modified", additions: 5, deletions: 0, changes: 5 }]); - } - if (url.includes("/commits/slopoff123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs")) return Response.json({ id: 991 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "slop-off-clear", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 91, title: "Add helper", state: "open", user: { login: "contributor" }, head: { sha: "slopoff123" }, labels: [], body: "Adds a helper." }, - }, - }); - - // Slop gate off → the previously persisted dashboard score is null-persisted, not left stale. - const cleared = await getPullRequest(env, "JSONbored/gittensory", 91); - expect(cleared?.slopRisk).toBeNull(); - expect(cleared?.slopBand).toBeNull(); - expect(refreshedFiles).toHaveLength(1); - }); - - it("#dup-winner: flag ON spares the lowest open sibling — no duplicate block, slop not penalized for the cluster", async () => { - // GITTENSORY_DUPLICATE_WINNER ON. A same-issue cluster of OPEN PRs (#91 winner, #92 loser) under - // duplicatePrGateMode: block. The winner (#91, lowest open number) must NOT be gate-blocked or slop- - // penalized as a duplicate — it is judged on its own merits. This drives the flag-ON branch of the - // processors gate path (isDupWinner) + the advisory duplicate-finding suppression. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_DUPLICATE_WINNER: "true" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - duplicatePrGateMode: "block", - slopGateMode: "advisory", - qualityGateMode: "block", - qualityGateMinScore: 95, - }); - // The shared issue + the HIGHER-numbered open sibling (#92) → forms the same-issue duplicate cluster. - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "Cache the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 91, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win91" }, labels: [], body: "Fixes #1\n\nValidation: npm test" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 92, title: "Also fix the cache", state: "open", user: { login: "other" }, author_association: "CONTRIBUTOR", head: { sha: "sib92" }, labels: [], body: "Fixes #1" }); - - let gatePatchBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/91/files")) return Response.json([{ filename: "src/cache.ts", status: "modified", additions: 12, deletions: 0, changes: 12 }]); - if (url.includes("/commits/win91/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "PATCH") { - gatePatchBody = JSON.parse(String(init?.body ?? "{}")) as typeof gatePatchBody; - return Response.json({ id: 960 }); - } - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 960 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "dup-winner-on", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 91, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win91" }, labels: [], body: "Fixes #1\n\nValidation: npm test" }, - }, - }); - - // Winner survives: a later duplicate sibling must not lower readiness below a blocking threshold. - expect(gatePatchBody.conclusion).not.toBe("failure"); - expect(gatePatchBody.output?.text ?? "").not.toContain("readiness_score_below_threshold"); - // The persisted advisory for the winner OMITS the duplicate finding (suppressed) — that is what suppresses - // the gate failure and the auto-close duplicate cause. - const winnerAdvisory = await env.DB.prepare("select findings_json from advisories where target_type = 'pull_request' and repo_full_name = ? and pull_number = ?").bind("JSONbored/gittensory", 91).first<{ findings_json: string }>(); - expect(winnerAdvisory?.findings_json ?? "").not.toContain("duplicate_pr_risk"); - }); - - it("#dup-winner: flag OFF keeps every same-issue sibling blocked (byte-identical) — the winner is also closed-eligible", async () => { - // Same cluster, flag OFF (default). The lowest open PR (#91) STILL gets the duplicate block + finding, - // exactly like today — no winner is spared. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - duplicatePrGateMode: "block", - slopGateMode: "advisory", - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "Cache the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 92, title: "Also fix the cache", state: "open", user: { login: "other" }, author_association: "CONTRIBUTOR", head: { sha: "sib92b" }, labels: [], body: "Fixes #1" }); - - let gatePatchBody: { conclusion?: string; output?: { title?: string; text?: string } } = {}; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/91/files")) return Response.json([{ filename: "src/cache.ts", status: "modified", additions: 12, deletions: 0, changes: 12 }]); - if (url.includes("/commits/win91b/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "PATCH") { - gatePatchBody = JSON.parse(String(init?.body ?? "{}")) as typeof gatePatchBody; - return Response.json({ id: 961 }); - } - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 961 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "dup-winner-off", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 91, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win91b" }, labels: [], body: "Fixes #1" }, - }, - }); - - // Flag OFF: the duplicate block still fires for the lowest sibling — the Gate fails, the finding persists. - expect(gatePatchBody.conclusion).toBe("failure"); - const winnerAdvisory = await env.DB.prepare("select findings_json from advisories where target_type = 'pull_request' and repo_full_name = ? and pull_number = ?").bind("JSONbored/gittensory", 91).first<{ findings_json: string }>(); - expect(winnerAdvisory?.findings_json ?? "").toContain("duplicate_pr_risk"); - }); - - it("REGRESSION (#dup-winner-slop-drift): maybePublishPrPublicSurface's slop penalty uses the LIVE-reconciled siblings, not a raw stale-cached read — a stale-cached-open lower sibling that is actually CLOSED on GitHub must not deny this PR winner status / slop-penalize it for the cluster", async () => { - // GITTENSORY_DUPLICATE_WINNER ON. PR #95 (this PR, being reviewed) links issue #1; PR #90 (LOWER-numbered, - // same linked issue) is cached `open` in the DB (a missed/delayed `closed` webhook), but GitHub's LIVE state - // for #90 is actually `closed`. Before the fix, maybePublishPrPublicSurface's own duplicate-winner election - // read the raw, un-reconciled `listPullRequests` result (still showing #90 as open) and so wrongly denied - // #95 winner status, applying the duplicateClusterMembership slop penalty (weight 15, persisted slop_band - // "low") even though the gate's OWN reconciled otherOpenPullRequests (used to build the advisory/gate - // disposition) had already correctly dropped #90. After the fix, both paths agree: #95 is the winner (no - // open siblings once reconciled) and carries NO duplicate-cluster slop penalty (slop_band "clean"). - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_DUPLICATE_WINNER: "true" }); - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - duplicatePrGateMode: "block", - slopGateMode: "advisory", - }); - await upsertIssueFromGitHub(env, "JSONbored/gittensory", { number: 1, title: "Cache the registry sync", state: "open", user: { login: "maintainer" }, author_association: "OWNER", body: "We should cache the registry fetch." }); - // Stale-cached-open sibling: the DB still says #90 is open (the closed webhook was missed/delayed). - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 90, title: "Older attempt at the cache fix", state: "open", user: { login: "other" }, author_association: "CONTRIBUTOR", head: { sha: "sib90" }, labels: [], body: "Fixes #1" }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 95, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win95" }, labels: [], body: "Fixes #1\n\nValidation: npm test" }); - - let liveStateFetches90 = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // The LIVE state of the lower sibling #90 is CLOSED, contradicting the stale-cached "open" DB row -- - // reconcileLiveDuplicateSiblings must discover this via a genuine live fetch, not the cache. - if (/\/pulls\/90(?:\?|$)/.test(url)) { - liveStateFetches90 += 1; - return Response.json({ number: 90, state: "closed" }); - } - // Includes a test-file change alongside the code change so missingTestEvidence never confounds the - // duplicateClusterMembership assertion below — this test isolates the ONE slop signal under test. - if (url.includes("/pulls/95/files")) - return Response.json([ - { filename: "src/cache.ts", status: "modified", additions: 12, deletions: 0, changes: 12 }, - { filename: "test/unit/cache.test.ts", status: "modified", additions: 8, deletions: 0, changes: 8 }, - ]); - if (url.includes("/commits/win95/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "PATCH") return Response.json({ id: 970 }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 970 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "dup-winner-slop-drift", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 95, title: "Fix the cache", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "win95" }, labels: [], body: "Fixes #1\n\nValidation: npm test" }, - }, - }); - - // A genuine live reconciliation happened (proving the fix reads live state, not the stale cache). - expect(liveStateFetches90).toBeGreaterThan(0); - // #95 is correctly credited as the cluster winner: no duplicateClusterMembership slop penalty persisted. - const winnerPr = await env.DB.prepare("select slop_risk, slop_band from pull_requests where repo_full_name = ? and number = ?").bind("JSONbored/gittensory", 95).first<{ slop_risk: number | null; slop_band: string | null }>(); - expect(winnerPr?.slop_band).toBe("clean"); - expect(winnerPr?.slop_risk).toBe(0); - }); - - it("overrides the Gate to neutral for THIS commit only when a real write/admin maintainer runs gate-override", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 90, - title: "Override me", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "override-sha" }, - labels: [], - body: "Validation: npm test", - }); - const calls = { token: 0, permission: 0, checkGets: 0, checkPatches: 0, commentGets: 0, commentPatches: 0 }; - const patchBodies: Array<{ status?: string; conclusion?: string; output?: { title?: string; text?: string } }> = []; - let confirmationBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - // Authorization MUST come from the real collaborator-permission API, never the comment author_association. - if (url.includes("/collaborators/maintainer/permission")) { - calls.permission += 1; - return Response.json({ permission: "admin" }); - } - if (url.includes("/commits/override-sha/check-runs") && method === "GET") { - calls.checkGets += 1; - return Response.json({ total_count: 1, check_runs: [{ id: 555, name: "Gittensory Orb Review Agent" }] }); - } - if (url.includes("/check-runs/555") && method === "PATCH") { - calls.checkPatches += 1; - patchBodies.push(JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string; text?: string } }); - return Response.json({ id: 555 }); - } - if (url.includes("/issues/90/comments") && method === "GET") { - calls.commentGets += 1; - return Response.json([]); - } - if (url.includes("/issues/90/comments") && method === "POST") { - calls.commentPatches += 1; - confirmationBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 9100 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-override-allow", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 90, title: "Override me", state: "open", user: { login: "contributor" }, pull_request: {} }, - // author_association lies (says OWNER); the handler must IGNORE it and use real permission instead. - comment: { id: 800, body: "@gittensory gate-override known flaky duplicate check, shipping", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - // The existing Gate run (id 555) was PATCHed to a neutral, non-blocking terminal state — not a new check. - expect(calls.checkPatches).toBe(1); - const finalize = patchBodies[0]; - expect(finalize?.status).toBe("completed"); - expect(finalize?.conclusion).toBe("neutral"); - expect(finalize?.output?.title).toBe("Gittensory Orb Review Agent — overridden by @maintainer"); - expect(finalize?.output?.text).toContain("Overridden by @maintainer: known flaky duplicate check, shipping"); - expect(confirmationBody).toContain("Gittensory Orb Review Agent overridden by @maintainer"); - const audit = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") - .bind("github_app.gate_overridden") - .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); - expect(audit).toMatchObject({ event_type: "github_app.gate_overridden", actor: "maintainer", target_key: "JSONbored/gittensory#90", outcome: "completed" }); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ surface: "github_app", eventName: "gate_overridden", outcome: "completed" })])); - // No override state is persisted: the gate stays "enabled" and the override does NOT persist an advisory, - // so a follow-up synchronize re-evaluates the Gate from scratch (no permanent bypass). - const settingsAfter = await env.DB.prepare("select gate_check_mode from repository_settings where repo_full_name = ?").bind("JSONbored/gittensory").first<{ gate_check_mode: string }>(); - expect(settingsAfter?.gate_check_mode).toBe("enabled"); - const overrideAdvisory = await env.DB.prepare("select id from advisories where target_key = ?").bind("JSONbored/gittensory#90").first<{ id: string }>(); - expect(overrideAdvisory ?? null).toBeNull(); - }); - - it("a real gate-override still completes even when the false-positive telemetry write fails (best-effort)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 94, - title: "Override me (telemetry write fails)", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "override-sha-telemetry" }, - labels: [], - body: "Validation: npm test", - }); - const telemetrySpy = vi.spyOn(repositoriesModule, "markGateOutcomeOverridden").mockRejectedValueOnce(new Error("D1 write failed")); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/commits/override-sha-telemetry/check-runs") && method === "GET") { - return Response.json({ total_count: 1, check_runs: [{ id: 559, name: "Gittensory Orb Review Agent" }] }); - } - if (url.includes("/check-runs/559") && method === "PATCH") return Response.json({ id: 559 }); - if (url.includes("/issues/94/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/94/comments") && method === "POST") return Response.json({ id: 9104 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-override-telemetry-fail", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 94, title: "Override me (telemetry write fails)", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 812, body: "@gittensory gate-override known flaky", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - expect(telemetrySpy).toHaveBeenCalled(); - // The override itself (audit + usage) still completed — the false-positive flag is best-effort and never - // affects the primary override outcome. - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? and target_key = ?") - .bind("github_app.gate_overridden", "JSONbored/gittensory#94") - .first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - }); - - it("gate-override respects agentPaused — never flips the live check-run or posts a confirmation comment (#2256)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - agentPaused: true, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 91, - title: "Override me while paused", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "paused-override-sha" }, - labels: [], - body: "Validation: npm test", - }); - const calls = { checkPatches: 0, commentPosts: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/commits/paused-override-sha/check-runs") && method === "GET") { - return Response.json({ total_count: 1, check_runs: [{ id: 556, name: "Gittensory Orb Review Agent" }] }); - } - if (url.includes("/check-runs/556") && method === "PATCH") { - calls.checkPatches += 1; - return Response.json({ id: 556 }); - } - if (url.includes("/issues/91/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/91/comments") && method === "POST") { - calls.commentPosts += 1; - return Response.json({ id: 9101 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-override-paused", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 91, title: "Override me while paused", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 810, body: "@gittensory gate-override please", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - // Neither write reached GitHub — a pause must stop this exactly like every other agent-driven write. - expect(calls.checkPatches).toBe(0); - expect(calls.commentPosts).toBe(0); - // REGRESSION: a paused command must not be audited/usage-tracked as a real, completed override. - const overridden = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.gate_overridden").first<{ id: string }>(); - expect(overridden).toBeUndefined(); - const skipped = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.gate_override_skipped").first<{ outcome: string; detail: string }>(); - expect(skipped).toMatchObject({ outcome: "completed", detail: "agent_paused" }); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ eventName: "gate_override_skipped", outcome: "skipped" })])); - expect(usageEvents.some((event) => event.eventName === "gate_overridden")).toBe(false); - }); - - it("gate-override respects agentDryRun on a PR with no head sha — records dry_run, not agent_paused (#2256)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - agentDryRun: true, - }); - // No head sha — also exercises the metadata's `?? null` fallback on the skip-path audit/usage records. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 93, - title: "Override me (dry-run, no head)", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: {}, - labels: [], - body: "Validation: npm test", - }); - const calls = { checkPatches: 0, commentPosts: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/pulls/93") && method === "GET") return Response.json({ number: 93, state: "open", head: {} }); - if (url.includes("/issues/93/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/93/comments") && method === "POST") { - calls.commentPosts += 1; - return Response.json({ id: 9103 }); - } - if (url.includes("/check-runs") && method === "PATCH") { - calls.checkPatches += 1; - return Response.json({ id: 557 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-override-dry-run-no-head", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 93, title: "Override me (dry-run, no head)", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 811, body: "@gittensory gate-override please", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - expect(calls.checkPatches).toBe(0); - expect(calls.commentPosts).toBe(0); - const skipped = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.gate_override_skipped") - .first<{ outcome: string; detail: string; metadata_json: string }>(); - expect(skipped).toMatchObject({ outcome: "completed", detail: "dry_run" }); - const metadata = JSON.parse(skipped?.metadata_json ?? "{}") as { headSha?: string | null; cachedHeadSha?: string | null; mode?: string }; - expect(metadata.headSha).toBeNull(); - expect(metadata.cachedHeadSha).toBeNull(); - expect(metadata.mode).toBe("dry_run"); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual(expect.arrayContaining([expect.objectContaining({ eventName: "gate_override_skipped", outcome: "skipped" })])); - }); - - it("overrides the LIVE head, not the stale cached SHA, when a commit landed after the command (#16)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - }); - // The stored row still carries the OLD head; a new commit ("live-sha") landed between the comment and now. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 90, - title: "Override me", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "stale-sha" }, - labels: [], - body: "Validation: npm test", - }); - const seen = { staleCheckGets: 0, liveCheckGets: 0, liveLegacyCheckGets: 0 }; - const patchBodies: Array<{ conclusion?: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - // The LIVE head re-fetch — the row says stale-sha but GitHub's head is now live-sha. - if (url.includes("/pulls/90") && method === "GET") return Response.json({ number: 90, state: "open", head: { sha: "live-sha" } }); - if (url.includes("/commits/stale-sha/check-runs") && method === "GET") { - seen.staleCheckGets += 1; - return Response.json({ total_count: 0, check_runs: [] }); - } - if (url.includes("/commits/live-sha/check-runs") && method === "GET") { - const checkName = new URL(url).searchParams.get("check_name"); - if (checkName === "Gittensory Gate") { - seen.liveLegacyCheckGets += 1; - return Response.json({ total_count: 0, check_runs: [] }); - } - seen.liveCheckGets += 1; - return Response.json({ total_count: 1, check_runs: [{ id: 556, name: "Gittensory Orb Review Agent" }] }); - } - if (url.includes("/check-runs/556") && method === "PATCH") { - patchBodies.push(JSON.parse(String(init?.body ?? "{}")) as { conclusion?: string }); - return Response.json({ id: 556 }); - } - if (url.includes("/issues/90/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/90/comments") && method === "POST") return Response.json({ id: 9101 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-override-live-head", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 90, title: "Override me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 803, body: "@gittensory gate-override flaky", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - // The neutral PATCH targeted the LIVE head's Gate run (id 556), and the stale SHA was never touched. - expect(seen.liveCheckGets).toBe(1); - expect(seen.liveLegacyCheckGets).toBe(1); - expect(seen.staleCheckGets).toBe(0); - expect(patchBodies[0]?.conclusion).toBe("neutral"); - const audit = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.gate_overridden") - .first<{ metadata_json: string }>(); - const metadata = JSON.parse(audit?.metadata_json ?? "{}") as { headSha?: string; cachedHeadSha?: string }; - expect(metadata.headSha).toBe("live-sha"); - expect(metadata.cachedHeadSha).toBe("stale-sha"); - }); - - it("records null head SHAs in the override audit when the PR head is unresolved (#16 fail-safe)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - }); - // A cached row with no head SHA (never detail-synced); the live fetch also yields no head. - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 90, - title: "Override me", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: {}, - labels: [], - body: "Validation: npm test", - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/pulls/90") && method === "GET") return Response.json({ number: 90, state: "open", head: {} }); - if (url.includes("/issues/90/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/90/comments") && method === "POST") return Response.json({ id: 9102 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-override-null-head", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 90, title: "Override me", state: "open", user: { login: "contributor" }, pull_request: {} }, - // No reason after the command — exercises the "No reason provided." fallback too. - comment: { id: 804, body: "@gittensory gate-override", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - const audit = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = ?") - .bind("github_app.gate_overridden") - .first<{ detail: string; metadata_json: string }>(); - expect(audit?.detail).toBe("No reason provided."); - const metadata = JSON.parse(audit?.metadata_json ?? "{}") as { headSha?: string | null; cachedHeadSha?: string | null }; - expect(metadata.headSha).toBeNull(); - expect(metadata.cachedHeadSha).toBeNull(); - }); - - it("ignores gate-override commands on edited comments", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 92, - title: "Edited override", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "edited-override" }, - labels: [], - body: "Validation: npm test", - }); - const calls = { token: 0, permission: 0, checkRuns: 0, comments: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - if (url.includes("/collaborators/")) { - calls.permission += 1; - return Response.json({ permission: "admin" }); - } - if (url.includes("/check-runs")) { - calls.checkRuns += 1; - return Response.json({ total_count: 1, check_runs: [{ id: 556, name: "Gittensory Orb Review Agent" }] }); - } - if (url.includes("/comments")) { - calls.comments += 1; - return Response.json([]); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-override-edited", - eventName: "issue_comment", - payload: { - action: "edited", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 92, title: "Edited override", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 802, body: "@gittensory gate-override edited by moderator", author_association: "OWNER", user: { login: "maintainer", type: "User" } }, - sender: { login: "moderator", type: "User" }, - }, - }); - - expect(calls.permission).toBe(0); - expect(calls.checkRuns).toBe(0); - expect(calls.comments).toBe(0); - const overridden = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.gate_overridden").first<{ id: string }>(); - expect(overridden ?? null).toBeNull(); - const skipped = await env.DB.prepare("select actor, detail from audit_events where event_type = ?").bind("github_app.gate_override_skipped").first<{ actor: string; detail: string }>(); - expect(skipped).toMatchObject({ actor: "moderator", detail: "unsupported_comment_action" }); - }); - - it("denies gate-override from an org member without real repository write/admin (ignores author_association)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 91, - title: "Cannot override", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "override-denied" }, - labels: [], - body: "Validation: npm test", - }); - const calls = { token: 0, permission: 0, checkGets: 0, checkPatches: 0, comments: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) { - calls.token += 1; - return Response.json({ token: "installation-token" }); - } - // Real permission is only "read" — even though the comment claims MEMBER, the Gate must NOT be touched. - if (url.includes("/collaborators/org-member/permission")) { - calls.permission += 1; - return Response.json({ permission: "read" }); - } - if (url.includes("/check-runs")) { - calls.checkGets += 1; - return new Response("not found", { status: 404 }); - } - if (url.includes("/comments")) { - calls.comments += 1; - return Response.json([]); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "gate-override-deny", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - issue: { number: 91, title: "Cannot override", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 801, body: "@gittensory gate-override trust me", author_association: "MEMBER", user: { login: "org-member", type: "User" } }, - sender: { login: "org-member", type: "User" }, - }, - }); - - // Authorization denied via real permission: no Gate check call and no comment were made. - expect(calls.permission).toBe(1); - expect(calls.checkGets).toBe(0); - expect(calls.checkPatches).toBe(0); - expect(calls.comments).toBe(0); - const denied = await env.DB.prepare("select event_type, actor, target_key, outcome, detail from audit_events where event_type = ?") - .bind("github_app.gate_override_denied") - .first<{ event_type: string; actor: string; target_key: string; outcome: string; detail: string }>(); - expect(denied).toMatchObject({ event_type: "github_app.gate_override_denied", actor: "org-member", target_key: "JSONbored/gittensory#91", outcome: "denied", detail: "not_maintainer_or_pr_author" }); - const overridden = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.gate_overridden").first<{ id: string }>(); - expect(overridden ?? null).toBeNull(); - }); - - // #1964 (record slice): `@gittensory resolve` records review-memory suppression signals for advisory warnings. - describe("@gittensory resolve (#1964)", () => { - async function seedResolvePr(env: Env, repoFullName: string, prNumber: number, headSha: string) { - const slash = repoFullName.indexOf("/"); - const owner = slash >= 0 ? repoFullName.slice(0, slash) : repoFullName; - const name = slash >= 0 ? repoFullName.slice(slash + 1) : repoFullName; - await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); - await upsertRepositorySettings(env, { - repoFullName, - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - requireLinkedIssue: true, - linkedIssueGateMode: "advisory", - aiReviewMode: "advisory", - }); - await upsertPullRequestFromGitHub(env, repoFullName, { - number: prNumber, - title: "Resolve me", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: headSha }, - labels: [], - body: "No linked issue on purpose", - }); - } - - it("records a suppression signal and finding_resolved when an authorized maintainer resolves a named warning with review.memory ON", async () => { - const repoFullName = "JSONbored/resolve-1964-a"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_MEMORY: "true", - }); - await seedResolvePr(env, repoFullName, 1964, "resolve-1964-a"); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - const calls = { permission: 0, checkPatches: 0, comments: 0 }; - let confirmationBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) { - calls.permission += 1; - return Response.json({ permission: "admin" }); - } - if (url.includes("/issues/1964/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/1964/comments") && method === "POST") { - calls.comments += 1; - confirmationBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 19641 }); - } - if (url.includes("/check-runs") && method === "PATCH") { - calls.checkPatches += 1; - return Response.json({ id: 1 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1964-allow", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1964-a", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1964, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { - id: 19640, - body: "@gittensory resolve missing_linked_issue", - author_association: "NONE", - user: { login: "maintainer", type: "User" }, - }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - expect(calls.permission).toBe(1); - expect(calls.checkPatches).toBe(0); - expect(calls.comments).toBe(1); - expect(confirmationBody).toContain("Review finding resolved"); - expect(confirmationBody).toContain("missing_linked_issue"); - expect(confirmationBody).toContain("Gate check-run is unchanged"); - const resolved = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?") - .bind("github_app.finding_resolved") - .first<{ outcome: string; detail: string }>(); - expect(resolved).toMatchObject({ outcome: "completed" }); - const memoryRecorded = await env.DB.prepare("select outcome from audit_events where event_type = ?") - .bind("github_app.review_memory_recorded") - .first<{ outcome: string }>(); - expect(memoryRecorded).toMatchObject({ outcome: "completed" }); - const suppressions = await listReviewSuppressions(env, repoFullName); - expect(suppressions).toHaveLength(1); - expect(suppressions[0]).toMatchObject({ - category: "missing_linked_issue", - createdBy: "maintainer", - }); - }); - - it("records a suppression for a current cached AI review warning", async () => { - const repoFullName = "JSONbored/resolve-1964-ai-cached"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); - await seedResolvePr(env, repoFullName, 1974, "resolve-1964-ai-cached"); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - await putCachedAiReview(env, repoFullName, 1974, "resolve-1964-ai-cached", "advisory", { - notes: "The cached AI review found a public issue.", - reviewerCount: 2, - findings: [{ code: "ai_review_split", severity: "warning", title: "AI reviewers disagree", detail: "One reviewer flagged a likely defect that needs maintainer triage." }], - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/1974/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/1974/comments") && method === "POST") return Response.json({ id: 19741 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1974-ai-cached", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1964-ai-cached", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1974, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19740, body: "@gittensory resolve ai_review_split", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - const suppressions = await listReviewSuppressions(env, repoFullName); - expect(suppressions).toHaveLength(1); - expect(suppressions[0]).toMatchObject({ category: "ai_review_split", createdBy: "maintainer" }); - const resolved = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.finding_resolved") - .first<{ metadata_json: string }>(); - expect(JSON.parse(resolved?.metadata_json ?? "{}")).toMatchObject({ findingCode: "ai_review_split", resolvedWarningCount: 1, recordedSuppressionCount: 1 }); - }); - - it("falls back to the last published public AI review when the current cached review has no public assessment", async () => { - const repoFullName = "JSONbored/resolve-1964-ai-published"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); - await seedResolvePr(env, repoFullName, 1975, "resolve-1964-ai-current"); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - await putCachedAiReview(env, repoFullName, 1975, "resolve-1964-ai-old", "advisory", { - notes: "The published AI review found a public consensus defect.", - reviewerCount: 2, - findings: [{ code: "ai_consensus_defect", severity: "warning", title: "AI reviewers agree on a defect", detail: "Both reviewers flagged the same likely defect for maintainer triage." }], - }); - await markAiReviewPublished(env, repoFullName, 1975, "resolve-1964-ai-old"); - await putCachedAiReview(env, repoFullName, 1975, "resolve-1964-ai-current", "advisory", { - notes: "", - reviewerCount: 2, - findings: [{ code: "ai_review_split", severity: "warning", title: "Hidden", detail: "No public assessment." }], - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/1975/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/1975/comments") && method === "POST") return Response.json({ id: 19751 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1975-ai-published", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1964-ai-published", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1975, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19750, body: "@gittensory resolve ai_consensus_defect", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - const suppressions = await listReviewSuppressions(env, repoFullName); - expect(suppressions).toHaveLength(1); - expect(suppressions[0]?.category).toBe("ai_consensus_defect"); - }); - - it("records finding_resolved without a suppression write when review.memory is OFF (operator kill-switch)", async () => { - const repoFullName = "JSONbored/resolve-1965-off"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedResolvePr(env, repoFullName, 1965, "resolve-1965-off"); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/1965/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/1965/comments") && method === "POST") return Response.json({ id: 19651 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1965-flag-off", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1965-off", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1965, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { - id: 19650, - body: "@gittensory resolve missing_linked_issue", - author_association: "NONE", - user: { login: "maintainer", type: "User" }, - }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - const memoryRecorded = await env.DB.prepare("select id from audit_events where event_type = ?") - .bind("github_app.review_memory_recorded") - .first<{ id: string }>(); - expect(memoryRecorded ?? null).toBeNull(); - expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); - const resolved = await env.DB.prepare("select outcome from audit_events where event_type = ?") - .bind("github_app.finding_resolved") - .first<{ outcome: string }>(); - expect(resolved).toMatchObject({ outcome: "completed" }); - }); - - it("denies an unauthorized actor and records no suppression signal", async () => { - const repoFullName = "JSONbored/resolve-1966-deny"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); - await seedResolvePr(env, repoFullName, 1966, "resolve-1966-deny"); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/org-member/permission")) return Response.json({ permission: "read" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1966-deny", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1966-deny", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1966, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { - id: 19660, - body: "@gittensory resolve missing_linked_issue", - author_association: "MEMBER", - user: { login: "org-member", type: "User" }, - }, - sender: { login: "org-member", type: "User" }, - }, - }); - - const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?") - .bind("github_app.finding_resolved_denied") - .first<{ outcome: string }>(); - expect(denied).toMatchObject({ outcome: "denied" }); - expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); - }); - - it.each([ - ["malformed finding id", "@gittensory resolve ../escape", "malformed_finding_id"], - ["absent finding code", "@gittensory resolve readiness_score_below_threshold", "finding_not_found"], - ] as const)("skips resolve when the maintainer supplies %s", async (_label, body, reason) => { - const repoFullName = "JSONbored/resolve-1967-skip"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); - await seedResolvePr(env, repoFullName, 1967, "resolve-1967-skip"); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: `resolve-1967-${reason}`, - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1967-skip", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1967, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19670, body, author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.finding_resolved_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe(reason); - expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); - }); - - it("records every current advisory warning for a whole-PR `@gittensory resolve` ack", async () => { - const repoFullName = "JSONbored/resolve-1968-whole"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); - await seedResolvePr(env, repoFullName, 1968, "resolve-1968-whole"); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/1968/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/1968/comments") && method === "POST") return Response.json({ id: 19681 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1968-whole", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1968-whole", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1968, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19680, body: "@gittensory resolve", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - - expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(2); - const resolved = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.finding_resolved") - .first<{ metadata_json: string }>(); - expect(JSON.parse(resolved?.metadata_json ?? "{}")).toMatchObject({ scope: "whole_pr", resolvedWarningCount: 2 }); - }); - - it("ignores issue comments that are not @gittensory resolve commands (#1964)", async () => { - const repoFullName = "JSONbored/resolve-1973-plain"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedResolvePr(env, repoFullName, 1973, "resolve-1973-plain"); - let commentPosts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/1973/comments") && method === "POST") { - commentPosts += 1; - return Response.json({ id: 19730 }); - } - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-plain-comment", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1973-plain", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1973, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19731, body: "Looks good to me", author_association: "OWNER", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - expect(commentPosts).toBe(0); - const events = await env.DB.prepare("select event_type from audit_events where event_type like ?").bind("github_app.finding_resolved%").all<{ event_type: string }>(); - expect(events.results ?? []).toEqual([]); - }); - - it("ignores other @gittensory verbs on the resolve handler path (#1964)", async () => { - const repoFullName = "JSONbored/resolve-1974-help"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedResolvePr(env, repoFullName, 1974, "resolve-1974-help"); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-help-verb", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1974-help", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1974, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19740, body: "@gittensory help", author_association: "OWNER", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - const events = await env.DB.prepare("select event_type from audit_events where event_type like ?").bind("github_app.finding_resolved%").all<{ event_type: string }>(); - expect(events.results ?? []).toEqual([]); - }); - - it("skips resolve when the webhook payload lacks a repository (#1964)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-missing-repo", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - issue: { number: 1972, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19720, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("missing_repo_pr_installation_or_actor"); - }); - - it("skips resolve when the cached pull request row is missing (#1964)", async () => { - const repoFullName = "JSONbored/resolve-1969-missing-pr"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); - const slash = repoFullName.indexOf("/"); - await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1969-missing-pr", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1969-missing-pr", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1969, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19690, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("cached_pr_missing"); - }); - - it("skips resolve in agentDryRun without recording finding_resolved (#1964)", async () => { - const repoFullName = "JSONbored/resolve-1970-dry-run"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); - await seedResolvePr(env, repoFullName, 1970, "resolve-1970-dry-run"); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: true, linkedIssueGateMode: "advisory", agentDryRun: true }); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1970-dry-run", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1970-dry-run", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1970, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19700, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("dry_run"); - const resolved = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.finding_resolved").first<{ id: string }>(); - expect(resolved ?? null).toBeNull(); - }); - - it("skips resolve when the repository is agentPaused (#1964)", async () => { - const repoFullName = "JSONbored/resolve-1971-paused"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); - await seedResolvePr(env, repoFullName, 1971, "resolve-1971-paused"); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: true, linkedIssueGateMode: "advisory", agentPaused: true }); - await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "resolve-1971-paused", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "resolve-1971-paused", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 1971, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 19710, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - }); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("agent_paused"); - }); - }); - - // #2169 (part of #1960): `@gittensory explain ` echoes an already-generated finding's public-safe - // rationale on the PR thread — read-only, no model call, no mutation. Mirrors the `resolve` harness above. - describe("@gittensory explain (#2169)", () => { - async function seedExplainPr(env: Env, repoFullName: string, prNumber: number, headSha: string) { - const slash = repoFullName.indexOf("/"); - const owner = repoFullName.slice(0, slash); - const name = repoFullName.slice(slash + 1); - await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: true, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); - await upsertPullRequestFromGitHub(env, repoFullName, { number: prNumber, title: "Explain me", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: headSha }, labels: [], body: "No linked issue on purpose" }); - } - const explainWebhook = (repoFullName: string, prNumber: number, body: string, actor: string, opts: { association?: string; bot?: boolean; action?: string } = {}) => ({ - type: "github-webhook" as const, - deliveryId: `explain-${prNumber}-${actor}`, - eventName: "issue_comment" as const, - payload: { - action: opts.action ?? "created", - installation: { id: 123, account: { login: repoFullName.slice(0, repoFullName.indexOf("/")), id: 1, type: "User" } }, - repository: { name: repoFullName.slice(repoFullName.indexOf("/") + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, repoFullName.indexOf("/")) } }, - issue: { number: prNumber, title: "Explain me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: prNumber * 10, body, author_association: opts.association ?? "NONE", user: { login: actor, type: opts.bot ? "Bot" : "User" } }, - sender: { login: actor, type: opts.bot ? "Bot" : "User" }, - }, - }) as unknown as Parameters[1]; - - it("echoes a named finding's stored rationale to an authorized maintainer + records finding_explained (no mutation)", async () => { - const repoFullName = "JSONbored/explain-2169-echo"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedExplainPr(env, repoFullName, 2169, "explain-2169-echo"); - await putCachedAiReview(env, repoFullName, 2169, "explain-2169-echo", "advisory", { - notes: "The cached AI review found a public issue.", - reviewerCount: 2, - findings: [{ code: "ai_review_split", severity: "warning", title: "AI reviewers disagree", detail: "One reviewer flagged a likely defect that needs maintainer triage." }], - }); - let postedBody = ""; - const calls = { comments: 0, checkPatches: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/2169/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/2169/comments") && method === "POST") { calls.comments += 1; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 21690 }); } - if (url.includes("/check-runs") && method === "PATCH") { calls.checkPatches += 1; return Response.json({ id: 1 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, explainWebhook(repoFullName, 2169, "@gittensory explain ai_review_split", "maintainer")); - - expect(calls.comments).toBe(1); - expect(calls.checkPatches).toBe(0); // read-only: never touches the gate check-run - expect(postedBody).toContain("Explanation of `ai_review_split`"); - expect(postedBody).toContain("AI reviewers disagree"); // the finding's stored title - expect(postedBody).toContain("One reviewer flagged a likely defect"); // its stored rationale, echoed verbatim - const explained = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("github_app.finding_explained").first<{ outcome: string; metadata_json: string }>(); - expect(explained?.outcome).toBe("completed"); - expect(JSON.parse(explained?.metadata_json ?? "{}")).toMatchObject({ findingCode: "ai_review_split", explainedCount: 1 }); - }); - - it("echoes a deterministic finding's rationale AND its suggested action", async () => { - const repoFullName = "JSONbored/explain-2169-action"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - // seedExplainPr sets requireLinkedIssue + a body with no linked issue, so the gate yields the deterministic - // `missing_linked_issue` warning, which carries a `detail` AND an `action` (src/rules/advisory.ts). - await seedExplainPr(env, repoFullName, 2176, "explain-2169-action"); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/2176/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/2176/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 21760 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, explainWebhook(repoFullName, 2176, "@gittensory explain missing_linked_issue", "maintainer")); - - expect(postedBody).toContain("No linked issue detected"); // title - expect(postedBody).toContain("Suggested action:"); // the finding's action is rendered - expect(postedBody).toContain("link it explicitly in the PR body"); // the action text, echoed - const explained = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.finding_explained").first<{ outcome: string }>(); - expect(explained?.outcome).toBe("completed"); - }); - - it("posts a public-safe not-found note when the finding id is unknown", async () => { - const repoFullName = "JSONbored/explain-2169-missing"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedExplainPr(env, repoFullName, 2170, "explain-2169-missing"); - let postedBody = ""; - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/2170/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/2170/comments") && method === "POST") { posted = true; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 21700 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, explainWebhook(repoFullName, 2170, "@gittensory explain readiness_score_below_threshold", "maintainer")); - - expect(posted).toBe(true); - expect(postedBody).toContain("No review finding `readiness_score_below_threshold`"); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_explained_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("finding_not_found"); - }); - - it.each([ - ["missing argument", "@gittensory explain", "missing_finding_argument"], - ["malformed finding id", "@gittensory explain ../escape", "malformed_finding_id"], - ] as const)("skips (no comment) when the maintainer supplies %s", async (_label, body, reason) => { - const repoFullName = "JSONbored/explain-2169-skip"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedExplainPr(env, repoFullName, 2171, "explain-2169-skip"); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/comments") && method === "POST") { posted = true; return Response.json({ id: 1 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, explainWebhook(repoFullName, 2171, body, "maintainer")); - - expect(posted).toBe(false); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_explained_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe(reason); - }); - - it("denies a non-maintainer — no explanation posted, records finding_explained_denied", async () => { - const repoFullName = "JSONbored/explain-2169-deny"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedExplainPr(env, repoFullName, 2172, "explain-2169-deny"); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/org-member/permission")) return Response.json({ permission: "read" }); - if (url.includes("/comments")) { posted = true; return Response.json({ id: 1 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, explainWebhook(repoFullName, 2172, "@gittensory explain ai_review_split", "org-member", { association: "MEMBER" })); - - expect(posted).toBe(false); - const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.finding_explained_denied").first<{ outcome: string }>(); - expect(denied).toMatchObject({ outcome: "denied" }); - }); - - it("records a classifier skip for a bot-authored explain command, never acting on it", async () => { - const repoFullName = "JSONbored/explain-2169-bot"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedExplainPr(env, repoFullName, 2173, "explain-2169-bot"); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - - await processJob(env, explainWebhook(repoFullName, 2173, "@gittensory explain ai_review_split", "some-bot[bot]", { bot: true })); - - expect(posted).toBe(false); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_explained_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("bot_author"); - }); - - it("skips with cached_pr_missing when the referenced PR is not in the local store", async () => { - const repoFullName = "JSONbored/explain-2169-nopr"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - // Register the repo + settings but NOT the PR row, so getPullRequest returns null. - await upsertRepositoryFromGitHub(env, { name: "explain-2169-nopr", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { repoFullName, gateCheckMode: "enabled", reviewCheckMode: "required", aiReviewMode: "advisory" }); - let posted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/comments")) posted = true; - return new Response("not found", { status: 404 }); - }); - - await processJob(env, explainWebhook(repoFullName, 2174, "@gittensory explain ai_review_split", "maintainer")); - - expect(posted).toBe(false); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_explained_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("cached_pr_missing"); - }); - - it("declines (returns false) for a non-explain comment and for a plain non-mention comment", async () => { - const repoFullName = "JSONbored/explain-2169-decline"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedExplainPr(env, repoFullName, 2175, "explain-2169-decline"); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/2175/comments") && !url.includes("POST")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, explainWebhook(repoFullName, 2175, "just a normal comment, no mention", "maintainer")); - await processJob(env, explainWebhook(repoFullName, 2175, "@gittensory configuration", "maintainer")); - - // The explain handler never claimed either comment — no explain audit rows at all. - const explainRows = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.finding_explained%'").first<{ n: number }>(); - expect(explainRows?.n).toBe(0); - }); - }); - - // #4195 (part of the #4189 E2E-test-generation epic): `@gittensory generate-tests` -- on-demand, - // MAINTAINER-ONLY AI-generated E2E test coverage, posted as its own reply comment. Mirrors the explain - // harness above (classify -> authorize -> act -> audit), but with the authorization tier deliberately - // narrowed to ["maintainer"] only -- no collaborator, no confirmed_miner -- and a real (mocked) model call. - describe("@gittensory generate-tests (#4195)", () => { - async function seedGenerateTestsPr( - env: Env, - repoFullName: string, - prNumber: number, - headSha: string, - authorLogin = "contributor", - opts: { headRef?: string; e2eTestDelivery?: "comment" | "commit" } = {}, - ) { - const slash = repoFullName.indexOf("/"); - const owner = repoFullName.slice(0, slash); - const name = repoFullName.slice(slash + 1); - await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); - await upsertPullRequestFromGitHub(env, repoFullName, { number: prNumber, title: "Add retry to checkout", state: "open", user: { login: authorLogin }, author_association: "CONTRIBUTOR", head: { sha: headSha, ref: opts.headRef ?? "feature/checkout-retry" }, labels: [], body: "Retries the payment call once on a 5xx." }); - await upsertPullRequestFile(env, { repoFullName, pullNumber: prNumber, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); - // A renamed-with-no-patch file (GitHub omits `patch` for pure renames) -- exercises the - // payload?.patch-is-not-a-string branch in the files.map() that builds E2eTestGenChangedFile[]. - await upsertPullRequestFile(env, { repoFullName, pullNumber: prNumber, path: "src/renamed.ts", status: "renamed", additions: 0, deletions: 0, changes: 0, payload: {} }); - // features.e2eTests + review.e2e_test_delivery MUST land in the SAME upsertRepoFocusManifest call -- - // a second separate call REPLACES rather than merges with a prior one (see repo-doc-pr.test.ts). - await upsertRepoFocusManifest(env, repoFullName, { - features: { e2eTests: true }, - ...(opts.e2eTestDelivery ? { review: { e2e_test_delivery: opts.e2eTestDelivery } } : {}), - }); - } - const generateTestsWebhook = (repoFullName: string, prNumber: number, actor: string, opts: { association?: string; bot?: boolean; commenterIsAuthor?: boolean } = {}) => ({ - type: "github-webhook" as const, - deliveryId: `generate-tests-${prNumber}-${actor}`, - eventName: "issue_comment" as const, - payload: { - action: "created", - installation: { id: 123, account: { login: repoFullName.slice(0, repoFullName.indexOf("/")), id: 1, type: "User" } }, - repository: { name: repoFullName.slice(repoFullName.indexOf("/") + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, repoFullName.indexOf("/")) } }, - issue: { number: prNumber, title: "Add retry to checkout", state: "open", user: { login: opts.commenterIsAuthor ? actor : "contributor" }, pull_request: {} }, - comment: { id: prNumber * 10, body: "@gittensory generate-tests", author_association: opts.association ?? "NONE", user: { login: actor, type: opts.bot ? "Bot" : "User" } }, - sender: { login: actor, type: opts.bot ? "Bot" : "User" }, - }, - }) as unknown as Parameters[1]; - const VALID_TEST_SOURCE = "import { test, expect } from '@playwright/test';\n\ntest('checkout retries on failure', async ({ page }) => {\n await page.goto('/checkout');\n await expect(page.getByRole('button', { name: 'Pay' })).toBeVisible();\n});"; - - it("generates and posts an E2E test for an authorized maintainer, and records a completed audit event", async () => { - const repoFullName = "JSONbored/gen-tests-4195-ok"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedGenerateTestsPr(env, repoFullName, 4195, "gen-tests-4195-ok"); - let postedBody = ""; - let posted = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/4195/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4195/comments") && method === "POST") { posted += 1; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 41950 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4195, "maintainer", { association: "MEMBER" })); - - expect(posted).toBe(1); - expect(postedBody).toContain("AI-generated Playwright test for @maintainer"); - expect(postedBody).toContain("test('checkout retries on failure'"); - const audited = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ outcome: string; metadata_json: string }>(); - expect(audited?.outcome).toBe("completed"); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ status: "ok", byok: false }); - }); - - it("denies a collaborator-tier actor (write permission, not the PR author) — narrower than every other command", async () => { - const repoFullName = "JSONbored/gen-tests-4195-collab"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedGenerateTestsPr(env, repoFullName, 4196, "gen-tests-4195-collab"); - let posted = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/writer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/4196/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4196/comments") && method === "POST") { posted += 1; return Response.json({ id: 41960 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4196, "writer", { association: "COLLABORATOR" })); - - expect(posted).toBe(0); // denied before any generation or comment - const denied = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_denied").first<{ outcome: string; detail: string }>(); - expect(denied?.outcome).toBe("denied"); - }); - - it("denies the PR's own author even though they authored it — the exact loophole a click-to-generate button must not open", async () => { - const repoFullName = "JSONbored/gen-tests-4195-author"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedGenerateTestsPr(env, repoFullName, 4197, "gen-tests-4195-author", "contributor"); - let posted = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // No collaborator/permission relationship at all -- a plain contributor commenting on their own PR. - if (url.includes("/collaborators/contributor/permission")) return new Response("not found", { status: 404 }); - if (url.includes("/issues/4197/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4197/comments") && method === "POST") { posted += 1; return Response.json({ id: 41970 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4197, "contributor", { association: "NONE", commenterIsAuthor: true })); - - expect(posted).toBe(0); - const denied = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_denied").first<{ detail: string }>(); - expect(denied?.detail).toBe("maintainer_command_requires_maintainer"); - }); - - it("falls back to a safe withheld-content note when posting the real generated-test comment fails", async () => { - const repoFullName = "JSONbored/gen-tests-4195-post-fails"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedGenerateTestsPr(env, repoFullName, 4200, "gen-tests-4195-post-fails"); - let postAttempts = 0; - let fallbackBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/4200/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4200/comments") && method === "POST") { - postAttempts += 1; - // The FIRST attempt (the real generated-test comment) fails with a genuine GitHub API error; the - // SECOND attempt (the withheld-content fallback) must still succeed. - if (postAttempts === 1) return new Response("server exploded", { status: 500 }); - fallbackBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 42000 }); - } - return new Response("not found", { status: 404 }); - }); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - - await processJob(env, generateTestsWebhook(repoFullName, 4200, "maintainer", { association: "MEMBER" })); - - expect(postAttempts).toBe(2); - expect(fallbackBody).toContain("did not produce a usable result"); - expect(fallbackBody).not.toContain("test('checkout retries on failure'"); - expect(logSpy.mock.calls.map((c) => String(c[0])).some((line) => line.includes("e2e_test_gen_comment_withheld"))).toBe(true); - logSpy.mockRestore(); - }); - - it("posts a not-enabled note (no generation call) when features.e2eTests is off for the repo", async () => { - const repoFullName = "JSONbored/gen-tests-4195-disabled"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - const slash = repoFullName.indexOf("/"); - await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); - await upsertPullRequestFromGitHub(env, repoFullName, { number: 4198, title: "x", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "gen-tests-4195-disabled" }, labels: [], body: "x" }); - // Deliberately no upsertRepoFocusManifest features.e2eTests override -- stays off (no allowlist either). - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/4198/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4198/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 41980 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4198, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("E2E test generation is not enabled for this repository"); - expect(run).not.toHaveBeenCalled(); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("feature_disabled"); - }); - - it("posts a did-not-produce-a-usable-result note when the model output never parses", async () => { - const repoFullName = "JSONbored/gen-tests-4195-garbage"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "not a test file" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedGenerateTestsPr(env, repoFullName, 4199, "gen-tests-4195-garbage"); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/4199/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4199/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 41990 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4199, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("did not produce a usable result"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ status: "ok" }); - }); - - it("skips cleanly when the cached PR record is missing", async () => { - const repoFullName = "JSONbored/gen-tests-4195-nopr"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - const slash = repoFullName.indexOf("/"); - await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); - // No upsertPullRequestFromGitHub -- the PR was never cached. - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4200, "maintainer", { association: "MEMBER" })); - - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("cached_pr_missing"); - }); - - it("declines (returns false) for a non-command comment, claiming nothing", async () => { - const repoFullName = "JSONbored/gen-tests-4195-decline"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedGenerateTestsPr(env, repoFullName, 4201, "gen-tests-4195-decline"); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/4201/comments")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - const webhook = generateTestsWebhook(repoFullName, 4201, "maintainer", { association: "MEMBER" }); - (webhook as unknown as { payload: { comment: { body: string } } }).payload.comment.body = "just chatting, no mention here"; - - await processJob(env, webhook); - - const rows = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); - expect(rows?.n).toBe(0); - }); - - it("skips cleanly when the comment classifies as invalid (a bot posted the mention)", async () => { - const repoFullName = "JSONbored/gen-tests-4195-bot"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedGenerateTestsPr(env, repoFullName, 4202, "gen-tests-4195-bot"); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4202, "some-bot[bot]", { association: "NONE", bot: true })); - - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("bot_author"); - }); - - it("uses the maintainer's BYOK frontier model (not Workers AI) when aiReviewByok is on and a key is configured", async () => { - const repoFullName = "JSONbored/gen-tests-4195-byok"; - const run = vi.fn(); // Workers AI must NOT be used when BYOK is configured - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - TOKEN_ENCRYPTION_SECRET: "gen-tests-byok-test-encryption-secret-32b", - }); - await seedGenerateTestsPr(env, repoFullName, 4203, "gen-tests-4195-byok"); - // aiReviewProvider set AND matching the stored key's provider -- exercises the "explicit provider - // pin agrees with the stored key" arm, distinct from the (also-tested-elsewhere) "no pin configured" - // default arm. - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory", aiReviewByok: true, aiReviewProvider: "anthropic" }); - await upsertRepositoryAiKey(env, { repoFullName, provider: "anthropic", key: "sk-ant-byok-gen-tests-9999", model: null }); - let postedBody = ""; - const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("api.anthropic.com")) return Response.json({ content: [{ type: "text", text: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }] }); - if (url.includes("/issues/4203/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4203/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42030 }); } - return new Response("not found", { status: 404 }); - }); - vi.stubGlobal("fetch", fetchMock); - - await processJob(env, generateTestsWebhook(repoFullName, 4203, "maintainer", { association: "MEMBER" })); - - expect(run).not.toHaveBeenCalled(); - expect(postedBody).toContain("test('checkout retries on failure'"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ byok: true }); - }); - - it("degrades to the not-usable-result note when the feature is on but no AI provider is configured at all", async () => { - const repoFullName = "JSONbored/gen-tests-4195-unavailable"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedGenerateTestsPr(env, repoFullName, 4204, "gen-tests-4195-unavailable"); // no env.AI, no BYOK key - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/4204/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4204/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42040 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4204, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("did not produce a usable result"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ status: "unavailable" }); - }); - - it("generates via the GITTENSORY_REVIEW_REPOS allowlist default when no manifest is published at all", async () => { - // No upsertRepoFocusManifest call -- loadRepoFocusManifest resolves null, so manifest?.review (fed to - // resolveE2eTestGenInstructions) and the e2eTests feature gate itself both take their null/allowlist path. - const repoFullName = "JSONbored/gen-tests-4195-allowlist"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - GITTENSORY_REVIEW_REPOS: repoFullName, - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - const slash = repoFullName.indexOf("/"); - await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); - await upsertPullRequestFromGitHub(env, repoFullName, { number: 4205, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "gen-tests-4195-allowlist" }, labels: [], body: "x" }); - await upsertPullRequestFile(env, { repoFullName, pullNumber: 4205, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/4205/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4205/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42050 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4205, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("test('checkout retries on failure'"); - }); - - it("skips cleanly when the webhook payload has no comment object at all", async () => { - const repoFullName = "JSONbored/gen-tests-4195-nocomment"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedGenerateTestsPr(env, repoFullName, 4206, "gen-tests-4195-nocomment"); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - const webhook = generateTestsWebhook(repoFullName, 4206, "maintainer", { association: "MEMBER" }); - delete (webhook as unknown as { payload: { comment?: unknown } }).payload.comment; - - await processJob(env, webhook); - - const rows = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); - expect(rows?.n).toBe(0); - }); - - // #4197 (commit delivery) + #4201 (scoring-integrity safeguard), both part of the #4189 epic. - describe("commit delivery mode (#4197, #4201)", () => { - it("pushes the generated test as a commit onto the PR's own head branch for a non-miner author, and records commitStatus: committed", async () => { - const repoFullName = "JSONbored/gen-tests-4197-commit-ok"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedGenerateTestsPr(env, repoFullName, 4207, "commit-ok-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.endsWith("/pulls/4207") && method === "GET") return Response.json({ head: { ref: "feature/checkout-retry", sha: "commit-ok-head-sha", repo: { full_name: repoFullName } } }); - if (url.endsWith("/git/commits/commit-ok-head-sha") && method === "GET") return Response.json({ tree: { sha: "base-tree-sha" } }); - if (url.endsWith("/git/trees") && method === "POST") return Response.json({ sha: "new-tree-sha" }); - if (url.endsWith("/git/commits") && method === "POST") return Response.json({ sha: "committed-sha-123" }); - if (method === "PATCH") return Response.json({}); - if (url.includes("/issues/4207/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4207/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42070 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4207, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("pushed as a commit"); - expect(postedBody).toContain(`https://github.com/${repoFullName}/commit/committed-sha-123`); - expect(postedBody).not.toContain("```typescript"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "committed" }); - }); - - it("blocks commit delivery for a confirmed Gittensor miner PR author, but still posts the generated test as a suggestion (#4201)", async () => { - const repoFullName = "JSONbored/gen-tests-4201-miner-blocked"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedGenerateTestsPr(env, repoFullName, 4208, "miner-blocked-head-sha", "confirmed-miner", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); - await upsertOfficialMinerDetection(env, "confirmed-miner", { status: "confirmed", snapshot: queueMinerSnapshot("confirmed-miner") }, 60_000); - let posted = 0; - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - // No git/trees or git/commits stubs at all -- a blocked commit must never even attempt a GitHub write. - if (url.includes("/issues/4208/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4208/comments") && method === "POST") { posted += 1; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42080 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4208, "maintainer", { association: "MEMBER" })); - - expect(posted).toBe(1); - expect(postedBody).toContain("confirmed Gittensor miner"); - expect(postedBody).toContain("```typescript\n" + VALID_TEST_SOURCE + "\n```"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "blocked" }); - }); - - it("falls back to a declined-with-reason suggestion when commit delivery has no write access to a fork PR branch", async () => { - const repoFullName = "JSONbored/gen-tests-4197-commit-declined"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedGenerateTestsPr(env, repoFullName, 4209, "declined-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.endsWith("/pulls/4209") && method === "GET") return new Response("forbidden", { status: 403 }); - if (url.includes("/issues/4209/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4209/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42090 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4209, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("Commit delivery was requested but declined: no write access"); - expect(postedBody).toContain("```typescript\n" + VALID_TEST_SOURCE + "\n```"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "declined" }); - }); - - it("declines commit delivery with a clear reason when the PR's head branch/commit is not cached at all", async () => { - const repoFullName = "JSONbored/gen-tests-4197-commit-no-head"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - const slash = repoFullName.indexOf("/"); - await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); - // No head sha/ref cached at all on this PR record. - await upsertPullRequestFromGitHub(env, repoFullName, { number: 4210, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", labels: [], body: "x" }); - await upsertPullRequestFile(env, { repoFullName, pullNumber: 4210, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); - await upsertRepoFocusManifest(env, repoFullName, { features: { e2eTests: true }, review: { e2e_test_delivery: "commit" } }); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/4210/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4210/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42100 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4210, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("Commit delivery was requested but declined: the PR's head branch/commit is not cached"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "declined" }); - }); - - it("maps a genuinely unexpected git-write failure to a declined outcome (not a thrown error) in the posted comment", async () => { - const repoFullName = "JSONbored/gen-tests-4197-commit-error-mapped"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedGenerateTestsPr(env, repoFullName, 4213, "error-mapped-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - // Neither a 403/404 (no write access) nor a 422/409 (branch moved) -- a genuinely unexpected 500, - // which commitE2eTestToPrBranch maps to status: "error" rather than "declined". - if (url.endsWith("/pulls/4213") && method === "GET") return new Response("server exploded", { status: 500 }); - if (url.includes("/issues/4213/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4213/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42130 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4213, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("Commit delivery was requested but declined:"); - expect(postedBody).toContain("```typescript\n" + VALID_TEST_SOURCE + "\n```"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "declined" }); - }); - - it("still resolves the miner-safeguard check (to not-found) when the cached PR record has no author login at all", async () => { - const repoFullName = "JSONbored/gen-tests-4201-no-author"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - const slash = repoFullName.indexOf("/"); - await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory" }); - // Deliberately no `user` field at all -- the cached PR's authorLogin resolves to null, exercising the - // ternary's not-found arm (`pr.authorLogin ? ... : { status: "not_found" }`) instead of ever calling - // getCachedOfficialMinerDetection. - await upsertPullRequestFromGitHub(env, repoFullName, { number: 4214, title: "Add retry to checkout", state: "open", author_association: "CONTRIBUTOR", head: { sha: "no-author-head-sha", ref: "feature/checkout-retry" }, labels: [], body: "x" }); - await upsertPullRequestFile(env, { repoFullName, pullNumber: 4214, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); - await upsertRepoFocusManifest(env, repoFullName, { features: { e2eTests: true }, review: { e2e_test_delivery: "commit" } }); - let postedBody = ""; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.endsWith("/pulls/4214") && method === "GET") return Response.json({ head: { ref: "feature/checkout-retry", sha: "no-author-head-sha", repo: { full_name: repoFullName } } }); - if (url.endsWith("/git/commits/no-author-head-sha") && method === "GET") return Response.json({ tree: { sha: "base-tree-sha" } }); - if (url.endsWith("/git/trees") && method === "POST") return Response.json({ sha: "new-tree-sha" }); - if (url.endsWith("/git/commits") && method === "POST") return Response.json({ sha: "no-author-commit-sha" }); - if (method === "PATCH") return Response.json({}); - if (url.includes("/issues/4214/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/4214/comments") && method === "POST") { postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 42140 }); } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4214, "maintainer", { association: "MEMBER" })); - - expect(postedBody).toContain("pushed as a commit"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "commit", commitStatus: "committed" }); - }); - - it("respects agentDryRun — never attempts commit delivery, and records dry_run (not agent_paused)", async () => { - const repoFullName = "JSONbored/gen-tests-4197-commit-dryrun"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedGenerateTestsPr(env, repoFullName, 4211, "dryrun-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory", agentDryRun: true }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4211, "maintainer", { association: "MEMBER" })); - - expect(run).not.toHaveBeenCalled(); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("dry_run"); - const generated = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ id: string }>(); - expect(generated ?? null).toBeNull(); - }); - - it("respects agentPaused — never attempts generation or commit delivery, and records agent_paused", async () => { - const repoFullName = "JSONbored/gen-tests-4197-commit-paused"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedGenerateTestsPr(env, repoFullName, 4212, "paused-head-sha", "contributor", { headRef: "feature/checkout-retry", e2eTestDelivery: "commit" }); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "advisory", aiReviewMode: "advisory", agentPaused: true }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, generateTestsWebhook(repoFullName, 4212, "maintainer", { association: "MEMBER" })); - - expect(run).not.toHaveBeenCalled(); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("agent_paused"); - }); - }); - }); - - // #4196 (part of the #4189 epic): promotes the existing manifest_missing_tests advisory finding into an - // actual auto-trigger for #4192/#4194's generation-and-render path, additive to the explicit - // `@gittensory generate-tests` command (#4195) tested above -- this describe block drives the AUTOMATED - // review pass (maybePublishPrPublicSurface, via a `pull_request` webhook) rather than an issue_comment. - describe("manifest_missing_tests auto-trigger (#4196)", () => { - const AUTO_TEST_SOURCE = "import { test, expect } from '@playwright/test';\n\ntest('auto-generated coverage', async ({ page }) => {\n await page.goto('/');\n await expect(page).toHaveTitle(/./);\n});"; - - async function seedAutoTriggerPr( - env: Env, - repoFullName: string, - prNumber: number, - headSha: string, - opts: { e2eTests?: boolean; hasTestFile?: boolean; validationNote?: boolean; manifestPolicyGateMode?: "advisory" | "block"; e2eTestDelivery?: "comment" | "commit"; autoTrigger?: boolean } = {}, - ) { - const slash = repoFullName.indexOf("/"); - const owner = repoFullName.slice(0, slash); - const name = repoFullName.slice(slash + 1); - await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); - await upsertRepositorySettings(env, { - repoFullName, - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - // reviewCheckMode: "required" (not "disabled") -- gateEnabled (which the whole manifestPolicyGateMode - // block this auto-trigger lives inside is downstream of) requires a truthy reviewCheckMode + a headSha. - // With reviewCheckMode: "disabled" the function bails out via its own early-return before ever reaching - // guidance. - gateCheckMode: "enabled", reviewCheckMode: "required", - requireLinkedIssue: false, - linkedIssueGateMode: "off", - manifestPolicyGateMode: opts.manifestPolicyGateMode ?? "advisory", - aiReviewMode: "off", - typeLabelsEnabled: false, - }); - await upsertPullRequestFromGitHub(env, repoFullName, { - number: prNumber, - title: "Add retry to checkout", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: headSha, ref: "feature/checkout-retry" }, - labels: [], - body: opts.validationNote ? "Ran npm run test:ci -- all green." : "No validation evidence mentioned here.", - }); - await upsertPullRequestFile(env, { - repoFullName, - pullNumber: prNumber, - path: opts.hasTestFile ? "test/unit/checkout.test.ts" : "src/checkout.ts", - status: "modified", - additions: 3, - deletions: 0, - changes: 3, - payload: { patch: "+function retryPayment() {\n+ return true;\n+}" }, - }); - // testExpectations is a TOP-LEVEL manifest field (unlike review.e2e_test_delivery's nested snake_case) -- - // both it and features.e2eTests must land in the SAME upsertRepoFocusManifest call, since a second - // separate call replaces rather than merges with the first. - // autoTrigger defaults to true here (NOT the production default) since this whole describe block exists - // to exercise the auto-trigger's own behavior -- the one test that cares about the real production - // default (OFF) passes `autoTrigger: false` explicitly, mirroring how the `e2eTests: false` case above - // already tests ITS OWN negative default the same way. - await upsertRepoFocusManifest(env, repoFullName, { - testExpectations: ["Run npm run test:ci."], - features: { e2eTests: opts.e2eTests ?? true }, - review: { e2e_test_auto_trigger: opts.autoTrigger ?? true, ...(opts.e2eTestDelivery ? { e2e_test_delivery: opts.e2eTestDelivery } : {}) }, - }); - } - - const autoTriggerWebhook = (repoFullName: string, prNumber: number, headSha: string, action: "opened" | "synchronize" = "opened", body = "No validation evidence mentioned here.") => ({ - type: "github-webhook" as const, - deliveryId: `auto-e2e-${prNumber}-${headSha}-${action}`, - eventName: "pull_request" as const, - payload: { - action, - installation: { id: 123, account: { login: repoFullName.slice(0, repoFullName.indexOf("/")), id: 1, type: "User" } }, - repository: { name: repoFullName.slice(repoFullName.indexOf("/") + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, repoFullName.indexOf("/")) } }, - pull_request: { - number: prNumber, - title: "Add retry to checkout", - state: "open", - user: { login: "contributor" }, - head: { sha: headSha }, - labels: [], - // The incoming webhook payload's own body ALWAYS re-upserts the cached PR record before this pass - // runs, overwriting whatever body seedAutoTriggerPr wrote directly to the DB -- so a test that needs - // a specific validation-note body must pass it here, not rely on the DB seed alone. - body, - }, - }, - }) as unknown as Parameters[1]; - - function stubAutoTriggerFetch(prNumber: number, posted: { count: number; body: string }) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // gateCheckMode: "enabled" means this pass ALSO publishes/updates a gate check-run -- these three - // endpoints back that unrelated publish, not the e2e-test-gen comment itself. - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: prNumber * 100 }, { status: 201 }); - if (url.includes("/check-runs") && method === "PATCH") return Response.json({ id: prNumber * 100, html_url: `https://github.com/checks/${prNumber * 100}` }); - if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { - posted.count += 1; - posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: prNumber * 10 }); - } - return new Response("not found", { status: 404 }); - }); - } - - it("auto-triggers generation when manifest_missing_tests fires and features.e2eTests is enabled", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-ok"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedAutoTriggerPr(env, repoFullName, 5001, "auto-4196-ok-sha"); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5001, posted); - - await processJob(env, autoTriggerWebhook(repoFullName, 5001, "auto-4196-ok-sha")); - - expect(posted.count).toBe(1); - expect(posted.body).toContain("test('auto-generated coverage'"); - const audited = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ outcome: string; metadata_json: string }>(); - expect(audited?.outcome).toBe("completed"); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ trigger: "auto", headSha: "auto-4196-ok-sha" }); - }); - - it("keeps the automated manifest_missing_tests trigger comment-only even when the manifest opts explicit commands into commit delivery", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-commit-forced-comment"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedAutoTriggerPr(env, repoFullName, 5011, "auto-4196-commit-forced-comment-sha", { e2eTestDelivery: "commit" }); - const posted = { count: 0, body: "" }; - const gitWrites: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 501100 }, { status: 201 }); - if (url.includes("/check-runs") && method === "PATCH") return Response.json({ id: 501100, html_url: "https://github.com/checks/501100" }); - if (url.includes("/git/trees") || url.includes("/git/commits") || url.includes("/git/refs/")) { - gitWrites.push(`${method} ${url}`); - return new Response("unexpected git write", { status: 500 }); - } - if (url.includes("/issues/5011/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/5011/comments") && method === "POST") { - posted.count += 1; - posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 50110 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, autoTriggerWebhook(repoFullName, 5011, "auto-4196-commit-forced-comment-sha")); - - expect(gitWrites).toEqual([]); - expect(posted.count).toBe(1); - expect(posted.body).toContain("test('auto-generated coverage'"); - const audited = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ metadata_json: string }>(); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ deliveryMode: "comment", trigger: "auto" }); - }); - - it("does not auto-trigger when manifest_missing_tests fires but features.e2eTests is disabled for the repo", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-disabled"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedAutoTriggerPr(env, repoFullName, 5002, "auto-4196-disabled-sha", { e2eTests: false }); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5002, posted); - - await processJob(env, autoTriggerWebhook(repoFullName, 5002, "auto-4196-disabled-sha")); - - expect(run).not.toHaveBeenCalled(); - expect(posted.count).toBe(0); - const audited = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ n: number }>(); - expect(audited?.n).toBe(0); - }); - - it("does not auto-trigger when features.e2eTests is enabled but review.e2e_test_auto_trigger is not set (safe default, #4196 separation)", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-no-opt-in"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - // e2eTests stays enabled (the master feature, which unlocks the command/checkbox) but autoTrigger is - // explicitly withheld -- the exact "enabled for maintainer-initiated use, but never fires unprompted" - // shape the feature must default to. - await seedAutoTriggerPr(env, repoFullName, 5012, "auto-4196-no-opt-in-sha", { autoTrigger: false }); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5012, posted); - - await processJob(env, autoTriggerWebhook(repoFullName, 5012, "auto-4196-no-opt-in-sha")); - - expect(run).not.toHaveBeenCalled(); - expect(posted.count).toBe(0); - const audited = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); - expect(audited?.n).toBe(0); - }); - - it("does not auto-trigger when the PR already carries a test file (the manifest_missing_tests signal never fires)", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-has-test"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedAutoTriggerPr(env, repoFullName, 5003, "auto-4196-has-test-sha", { hasTestFile: true }); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5003, posted); - - await processJob(env, autoTriggerWebhook(repoFullName, 5003, "auto-4196-has-test-sha")); - - expect(run).not.toHaveBeenCalled(); - expect(posted.count).toBe(0); - }); - - it("does not auto-trigger when the PR body already carries a validation note (the manifest_missing_tests signal never fires)", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-validated"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedAutoTriggerPr(env, repoFullName, 5004, "auto-4196-validated-sha", { validationNote: true }); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5004, posted); - - await processJob(env, autoTriggerWebhook(repoFullName, 5004, "auto-4196-validated-sha", "opened", "Ran npm run test:ci -- all green.")); - - expect(run).not.toHaveBeenCalled(); - expect(posted.count).toBe(0); - }); - - it("does not re-trigger generation on a second automated pass over the SAME unchanged head SHA (double-generation guard)", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-dedup"; - let runCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { runCalls += 1; return { response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }; } } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedAutoTriggerPr(env, repoFullName, 5005, "auto-4196-dedup-sha"); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5005, posted); - - // Two passes over the identical head SHA -- e.g. a `synchronize` redelivery or a re-review sweep tick - // with no new push in between. - await processJob(env, autoTriggerWebhook(repoFullName, 5005, "auto-4196-dedup-sha", "opened")); - await processJob(env, autoTriggerWebhook(repoFullName, 5005, "auto-4196-dedup-sha", "synchronize")); - - expect(runCalls).toBe(1); - expect(posted.count).toBe(1); - const rows = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ n: number }>(); - expect(rows?.n).toBe(1); - }); - - it("DOES trigger again for a genuinely NEW head SHA (a real push) even though a prior SHA on the same PR already fired", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-new-push"; - let runCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { runCalls += 1; return { response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }; } } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedAutoTriggerPr(env, repoFullName, 5006, "auto-4196-first-sha"); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5006, posted); - await processJob(env, autoTriggerWebhook(repoFullName, 5006, "auto-4196-first-sha", "opened")); - expect(runCalls).toBe(1); - - // A genuine new push: the PR's cached head SHA moves, re-seeding the manifest (features.e2eTests stays - // on) and re-running the webhook at the NEW sha. - await upsertPullRequestFromGitHub(env, repoFullName, { number: 5006, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, author_association: "CONTRIBUTOR", head: { sha: "auto-4196-second-sha", ref: "feature/checkout-retry" }, labels: [], body: "No validation evidence mentioned here." }); - await processJob(env, autoTriggerWebhook(repoFullName, 5006, "auto-4196-second-sha", "synchronize")); - - expect(runCalls).toBe(2); - expect(posted.count).toBe(2); - }); - - it("an explicit @gittensory generate-tests command still regenerates on the SAME head SHA the auto-trigger already covered", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-explicit-after-auto"; - let runCalls = 0; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => { runCalls += 1; return { response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }; } } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedAutoTriggerPr(env, repoFullName, 5007, "auto-4196-explicit-sha"); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5007, posted); - await processJob(env, autoTriggerWebhook(repoFullName, 5007, "auto-4196-explicit-sha")); - expect(runCalls).toBe(1); - - // Now the maintainer explicitly asks, on the SAME PR at the SAME (still-unpushed) head SHA. The - // auto-trigger's dedup guard must not leak into the explicit command's own path. - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/issues/5007/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/5007/comments") && method === "POST") { posted.count += 1; posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 50070 }); } - return new Response("not found", { status: 404 }); - }); - await processJob(env, { - type: "github-webhook", - deliveryId: "auto-e2e-4196-explicit-command", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "auto-e2e-4196-explicit-after-auto", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - issue: { number: 5007, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { id: 50071, body: "@gittensory generate-tests", author_association: "MEMBER", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - } as unknown as Parameters[1]); - - expect(runCalls).toBe(2); - expect(posted.count).toBe(2); - const rows = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ n: number }>(); - expect(rows?.n).toBe(2); - }); - - it("respects agentPaused — records a skip and never spends an LLM call, even though the signal fired", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-paused"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedAutoTriggerPr(env, repoFullName, 5008, "auto-4196-paused-sha"); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off", agentPaused: true }); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5008, posted); - - await processJob(env, autoTriggerWebhook(repoFullName, 5008, "auto-4196-paused-sha")); - - expect(run).not.toHaveBeenCalled(); - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("agent_paused"); - }); - - it("respects agentDryRun — records a skip with detail dry_run (not agent_paused), and never spends an LLM call", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-dryrun"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedAutoTriggerPr(env, repoFullName, 5009, "auto-4196-dryrun-sha"); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off", agentDryRun: true }); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5009, posted); - - await processJob(env, autoTriggerWebhook(repoFullName, 5009, "auto-4196-dryrun-sha")); - - expect(run).not.toHaveBeenCalled(); - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_skipped").first<{ detail: string }>(); - expect(skipped?.detail).toBe("dry_run"); - }); - - it("attributes the generated test to \"the PR author\" when the cached PR has no author login at all (a ghost/deleted account)", async () => { - const repoFullName = "JSONbored/auto-e2e-4196-no-author"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + AUTO_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - const slash = repoFullName.indexOf("/"); - await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", reviewCheckMode: "required", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off" }); - // Deliberately no `user` field at all -- authorLogin resolves to null, exercising the `author ?? "the PR - // author"` fallback arm (the explicit command's own `actor` is always a real commenter login, so this - // branch is reachable only from the auto-trigger, which has no comment-invoker to fall back on). - await upsertPullRequestFromGitHub(env, repoFullName, { number: 5010, title: "Add retry to checkout", state: "open", author_association: "CONTRIBUTOR", head: { sha: "auto-4196-no-author-sha", ref: "feature/checkout-retry" }, labels: [], body: "No validation evidence mentioned here." }); - await upsertPullRequestFile(env, { repoFullName, pullNumber: 5010, path: "src/checkout.ts", status: "modified", additions: 3, deletions: 0, changes: 3, payload: { patch: "+function retryPayment() {\n+ return true;\n+}" } }); - await upsertRepoFocusManifest(env, repoFullName, { testExpectations: ["Run npm run test:ci."], features: { e2eTests: true }, review: { e2e_test_auto_trigger: true } }); - const posted = { count: 0, body: "" }; - stubAutoTriggerFetch(5010, posted); - - // Built inline (not via autoTriggerWebhook) so the incoming payload's own pull_request sub-object omits - // `user` too -- autoTriggerWebhook always hardcodes a real `user.login`, which would re-upsert (and thus - // restore) an author login before this pass ever runs. - await processJob(env, { - type: "github-webhook", - deliveryId: "auto-e2e-4196-no-author", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "auto-e2e-4196-no-author", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 5010, title: "Add retry to checkout", state: "open", head: { sha: "auto-4196-no-author-sha" }, labels: [], body: "No validation evidence mentioned here." }, - }, - } as unknown as Parameters[1]); - - expect(posted.count).toBe(1); - expect(posted.body).toContain("AI-generated Playwright test for @the PR author"); - }); - }); - - // #4589: the interactive counterpart to #4583's text-only CTA. Same issue_comment.edited detection shell as - // the pre-existing "PR-panel retrigger" checkbox (marker presence, bot's-own-comment confirmation, bot-sender - // guard, payload.sender as the real actor re-authorized server-side), but dispatches through the SAME shared - // runE2eTestGenerationAndDeliver core the command (#4195) and auto-trigger (#4196) above already use. - describe("PR-panel generate-tests checkbox (#4589)", () => { - const CHECKBOX_TEST_SOURCE = "import { test, expect } from '@playwright/test';\n\ntest('checkbox-generated coverage', async ({ page }) => {\n await page.goto('/');\n await expect(page).toHaveTitle(/./);\n});"; - - async function seedCheckboxPr( - env: Env, - repoFullName: string, - prNumber: number, - headSha: string, - opts: { e2eTests?: boolean } = {}, - ) { - const slash = repoFullName.indexOf("/"); - const owner = repoFullName.slice(0, slash); - const name = repoFullName.slice(slash + 1); - await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); - await upsertRepositorySettings(env, { - repoFullName, - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - requireLinkedIssue: false, - linkedIssueGateMode: "off", - manifestPolicyGateMode: "advisory", - aiReviewMode: "off", - typeLabelsEnabled: false, - }); - await upsertPullRequestFromGitHub(env, repoFullName, { - number: prNumber, - title: "Add retry to checkout", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: headSha, ref: "feature/checkout-retry" }, - labels: [], - body: "No validation evidence mentioned here.", - }); - await upsertPullRequestFile(env, { - repoFullName, - pullNumber: prNumber, - path: "src/checkout.ts", - status: "modified", - additions: 3, - deletions: 0, - changes: 3, - payload: { patch: "+function retryPayment() {\n+ return true;\n+}" }, - }); - await upsertRepoFocusManifest(env, repoFullName, { - testExpectations: ["Run npm run test:ci."], - features: { e2eTests: opts.e2eTests ?? true }, - }); - } - - const CHECKED_GENERATE_TESTS_PANEL = [ - "", - "", - "- [x] Generate an AI Playwright test for this PR", - ].join("\n"); - - function checkboxWebhook( - repoFullName: string, - prNumber: number, - commentId: number, - sender: { login: string; type?: "User" | "Bot" }, - opts: { body?: string; commentUser?: { login: string; type: "User" | "Bot" }; omitInstallation?: boolean; omitPullRequest?: boolean } = {}, - ) { - const slash = repoFullName.indexOf("/"); - return { - type: "github-webhook" as const, - deliveryId: `checkbox-${prNumber}-${commentId}`, - eventName: "issue_comment" as const, - payload: { - action: "edited", - ...(opts.omitInstallation ? {} : { installation: { id: 123, account: { login: repoFullName.slice(0, slash), id: 1, type: "User" } } }), - repository: { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, - issue: { number: prNumber, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, ...(opts.omitPullRequest ? {} : { pull_request: {} }) }, - comment: { id: commentId, body: opts.body ?? CHECKED_GENERATE_TESTS_PANEL, user: opts.commentUser ?? { login: "gittensory[bot]", type: "Bot" } }, - sender: { login: sender.login, type: sender.type ?? "User" }, - }, - } as unknown as Parameters[1]; - } - - function stubCheckboxFetch(prNumber: number, actorLogin: string, permission: string, posted: { count: number; body: string }) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes(`/collaborators/${actorLogin}/permission`)) return Response.json({ permission }); - if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") { - posted.count += 1; - posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: prNumber * 10 }); - } - return new Response("not found", { status: 404 }); - }); - } - - it("dispatches generation when a maintainer checks the box", async () => { - const repoFullName = "JSONbored/checkbox-4589-ok"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + CHECKBOX_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedCheckboxPr(env, repoFullName, 6001, "checkbox-4589-ok-sha"); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6001, "maintainer", "admin", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6001, 900, { login: "maintainer" })); - - expect(posted.count).toBe(1); - expect(posted.body).toContain("test('checkbox-generated coverage'"); - const audited = await env.DB.prepare("select outcome, actor, metadata_json from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation") - .first<{ outcome: string; actor: string; metadata_json: string }>(); - expect(audited?.outcome).toBe("completed"); - expect(audited?.actor).toBe("maintainer"); - expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ trigger: "checkbox" }); - }); - - it("is a silent no-op when a non-maintainer checks the box — no comment posted, only a denial audit event", async () => { - const repoFullName = "JSONbored/checkbox-4589-denied"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: vi.fn() } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedCheckboxPr(env, repoFullName, 6002, "checkbox-4589-denied-sha"); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6002, "drive-by-user", "read", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6002, 901, { login: "drive-by-user" })); - - expect(posted.count).toBe(0); - const denied = await env.DB.prepare("select actor, outcome, detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_denied") - .first<{ actor: string; outcome: string; detail: string }>(); - expect(denied).toMatchObject({ actor: "drive-by-user", outcome: "denied" }); - }); - - // Authorization used to be hardcoded to maintainer-only here, ignoring whatever a repo's own - // .gittensory.yml commandAuthorization configured -- a self-hoster who wants their contributors to be - // able to trigger test generation had no way to widen it. It now respects settings.commandAuthorization, - // the exact same resolved (and safely clamped) policy the text-command version already uses. - it("dispatches generation for a COLLABORATOR (not just a maintainer) once the repo widens commandAuthorization for generate-tests", async () => { - const repoFullName = "JSONbored/checkbox-4589-widened"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + CHECKBOX_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedCheckboxPr(env, repoFullName, 6013, "checkbox-4589-widened-sha"); - await upsertRepositorySettings(env, { - repoFullName, - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - requireLinkedIssue: false, - linkedIssueGateMode: "off", - manifestPolicyGateMode: "advisory", - aiReviewMode: "off", - commandAuthorization: { default: ["maintainer"], commands: { "generate-tests": ["maintainer", "collaborator"] } }, - }); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6013, "collab-user", "write", posted); // "write" permission resolves to the COLLABORATOR association - - await processJob(env, checkboxWebhook(repoFullName, 6013, 911, { login: "collab-user" })); - - expect(posted.count).toBe(1); - expect(posted.body).toContain("test('checkbox-generated coverage'"); - const audited = await env.DB.prepare("select outcome, actor from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation") - .first<{ outcome: string; actor: string }>(); - expect(audited).toMatchObject({ outcome: "completed", actor: "collab-user" }); - }); - - it("still denies the PR's own author even if the repo tries to configure the raw pr_author role for generate-tests (safety clamp holds)", async () => { - const repoFullName = "JSONbored/checkbox-4589-clamped"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: vi.fn() } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - // seedCheckboxPr's own PR fixture is authored by "contributor" -- the SAME login checks the box below. - await seedCheckboxPr(env, repoFullName, 6014, "checkbox-4589-clamped-sha"); - await upsertRepositorySettings(env, { - repoFullName, - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "off", - requireLinkedIssue: false, - linkedIssueGateMode: "off", - manifestPolicyGateMode: "advisory", - aiReviewMode: "off", - // A repo attempting to grant its own PR authors unconditional access -- normalizeCommandRoleList drops - // the spoofable raw pr_author role for any MAINTAINER_ONLY_DEFAULT_COMMANDS entry (generate-tests is - // one), re-clamped at the point of use regardless of what's stored here. - commandAuthorization: { default: ["maintainer"], commands: { "generate-tests": ["pr_author"] } }, - }); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6014, "contributor", "read", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6014, 912, { login: "contributor" })); - - expect(posted.count).toBe(0); - const denied = await env.DB.prepare("select actor, outcome from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_denied") - .first<{ actor: string; outcome: string }>(); - expect(denied).toMatchObject({ actor: "contributor", outcome: "denied" }); - }); - - it("skips a bot-initiated edit (the bot's own comment re-render) without dispatching generation", async () => { - const repoFullName = "JSONbored/checkbox-4589-bot"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedCheckboxPr(env, repoFullName, 6003, "checkbox-4589-bot-sha"); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6003, "gittensory[bot]", "admin", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6003, 902, { login: "gittensory[bot]", type: "Bot" })); - - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("bot_author"); - }); - - it("ignores the marker when it appears in a comment that isn't the bot's own", async () => { - const repoFullName = "JSONbored/checkbox-4589-not-bot-comment"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedCheckboxPr(env, repoFullName, 6009, "checkbox-4589-not-bot-comment-sha"); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6009, "maintainer", "admin", posted); - - await processJob( - env, - checkboxWebhook(repoFullName, 6009, 908, { login: "maintainer" }, { commentUser: { login: "someone-else", type: "User" } }), - ); - - expect(posted.count).toBe(0); - const events = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); - expect(events?.n).toBe(0); - }); - - it("skips when features.e2eTests is disabled for the repo, even though the checkbox was checked", async () => { - const repoFullName = "JSONbored/checkbox-4589-disabled"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: vi.fn() } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedCheckboxPr(env, repoFullName, 6004, "checkbox-4589-disabled-sha", { e2eTests: false }); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6004, "maintainer", "admin", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6004, 903, { login: "maintainer" })); - - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("feature_disabled"); - }); - - it("ignores an edit where the generate-tests marker isn't checked (e.g. only the re-run box was checked)", async () => { - const repoFullName = "JSONbored/checkbox-4589-other-marker"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedCheckboxPr(env, repoFullName, 6005, "checkbox-4589-other-marker-sha"); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6005, "maintainer", "admin", posted); - const otherPanel = [ - "", - "", - "- [x] Re-run Gittensory review", - "- [ ] Generate an AI Playwright test for this PR", - ].join("\n"); - - await processJob(env, checkboxWebhook(repoFullName, 6005, 904, { login: "maintainer" }, { body: otherPanel })); - - expect(posted.count).toBe(0); - const events = await env.DB.prepare("select count(*) as n from audit_events where event_type like 'github_app.e2e_tests_generation%'").first<{ n: number }>(); - expect(events?.n).toBe(0); - }); - - it("skips a malformed payload (no installation / not a PR comment) without throwing", async () => { - const repoFullName = "JSONbored/checkbox-4589-malformed"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedCheckboxPr(env, repoFullName, 6006, "checkbox-4589-malformed-sha"); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6006, "maintainer", "admin", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6006, 905, { login: "maintainer" }, { omitPullRequest: true })); - - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("missing_repo_pr_or_installation"); - }); - - it("skips when the cached PR record is missing", async () => { - const repoFullName = "JSONbored/checkbox-4589-no-pr"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - // Repo registered but NO PR ever upserted -- getPullRequest resolves null. - await upsertRepositoryFromGitHub(env, { name: "checkbox-4589-no-pr", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, 123); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6007, "maintainer", "admin", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6007, 906, { login: "maintainer" })); - - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("cached_pr_missing"); - }); - - it("respects agentPaused — records a skip and never spends an LLM call, even though an authorized maintainer checked the box", async () => { - const repoFullName = "JSONbored/checkbox-4589-paused"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedCheckboxPr(env, repoFullName, 6008, "checkbox-4589-paused-sha"); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "off", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off", agentPaused: true }); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6008, "maintainer", "admin", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6008, 907, { login: "maintainer" })); - - expect(run).not.toHaveBeenCalled(); - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("agent_paused"); - }); - - it("respects agentDryRun — records a skip with detail dry_run (not agent_paused)", async () => { - const repoFullName = "JSONbored/checkbox-4589-dryrun"; - const run = vi.fn(); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), AI: { run } as unknown as Ai, GITTENSORY_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); - await seedCheckboxPr(env, repoFullName, 6010, "checkbox-4589-dryrun-sha"); - await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "off", requireLinkedIssue: false, linkedIssueGateMode: "off", manifestPolicyGateMode: "advisory", aiReviewMode: "off", agentDryRun: true }); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6010, "maintainer", "admin", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6010, 909, { login: "maintainer" })); - - expect(run).not.toHaveBeenCalled(); - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("dry_run"); - }); - - it("respects the repo's configured commit delivery mode via the checkbox (NOT forced comment-only, unlike the auto-trigger)", async () => { - const repoFullName = "JSONbored/checkbox-4589-commit"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - AI: { run: async () => ({ response: "```typescript\n" + CHECKBOX_TEST_SOURCE + "\n```" }) } as unknown as Ai, - GITTENSORY_REVIEW_E2E_TESTS: "true", - AI_SUMMARIES_ENABLED: "true", - AI_PUBLIC_COMMENTS_ENABLED: "true", - }); - await seedCheckboxPr(env, repoFullName, 6011, "checkbox-4589-commit-sha"); - await upsertRepoFocusManifest(env, repoFullName, { - testExpectations: ["Run npm run test:ci."], - features: { e2eTests: true }, - review: { e2e_test_delivery: "commit" }, - }); - const posted = { count: 0, body: "" }; - const gitWrites: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.endsWith("/pulls/6011") && method === "GET") { - return Response.json({ head: { ref: "feature/checkout-retry", sha: "checkbox-4589-commit-sha", repo: { full_name: repoFullName } } }); - } - if (url.endsWith("/git/commits/checkbox-4589-commit-sha") && method === "GET") return Response.json({ tree: { sha: "base-tree" } }); - if (url.endsWith("/git/trees") && method === "POST") { - gitWrites.push("tree"); - return Response.json({ sha: "new-tree" }); - } - if (url.endsWith("/git/commits") && method === "POST") { - gitWrites.push("commit"); - return Response.json({ sha: "new-commit" }); - } - if (method === "PATCH") { - gitWrites.push("ref"); - return Response.json({}); - } - if (url.includes("/issues/6011/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/6011/comments") && method === "POST") { - posted.count += 1; - posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 60110 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, checkboxWebhook(repoFullName, 6011, 910, { login: "maintainer" })); - - expect(gitWrites).toEqual(["tree", "commit", "ref"]); - expect(posted.count).toBe(1); - expect(posted.body).toContain("pushed as a commit"); - }); - - it("renders the checkbox (and the Test coverage collapsible) in the main review comment for a detected contributor missing tests", async () => { - const repoFullName = "JSONbored/checkbox-4589-full-panel"; - const env = createTestEnv({ - GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), - GITTENSORY_REVIEW_E2E_TESTS: "true", - // The checkbox/collapsible only render via the CONVERGED comment builder (buildUnifiedCommentBody); - // the legacy buildPublicPrIntelligenceComment path has neither and must be opted out of here too. - GITTENSORY_REVIEW_UNIFIED_COMMENT: "true", - }); - const slash = repoFullName.indexOf("/"); - await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName, - commentMode: "detected_contributors_only", - publicAudienceMode: "gittensor_only", - publicSurface: "comment_and_label", - autoLabelEnabled: false, - checkRunMode: "off", - // gateCheckMode MUST be "enabled" (not "off") -- maybePublishPrPublicSurface only takes the UNIFIED - // renderer branch when BOTH unifiedCommentAllowed AND gateEvaluation are truthy; gateEvaluation is - // never computed at all when the gate is off, silently falling back to the legacy panel (which has - // neither the Test coverage collapsible nor the generate-tests checkbox). Mirrors the settings shape - // of the pre-existing "renders the unified PR-review comment..." test above. - gateCheckMode: "enabled", reviewCheckMode: "required", - requireLinkedIssue: false, - linkedIssueGateMode: "off", - manifestPolicyGateMode: "advisory", - aiReviewMode: "off", - typeLabelsEnabled: false, - }); - await upsertRepoFocusManifest(env, repoFullName, { testExpectations: ["Run npm run test:ci."], features: { e2eTests: true, unifiedComment: true } }); - // gateEvaluation needs a resolved CI aggregate (mocking the module function directly is far simpler than - // stubbing every raw status/check-suite endpoint the live CI aggregator would otherwise call) -- but - // NOT "passed": resolveManifestPassedValidationCount treats a fully-green live CI rollup as validation - // evidence in its own right (`liveCi.ciState === "passed" ? 1 : 0`), which would satisfy - // manifest_missing_tests's own passedValidationCount check and suppress the very finding this test needs - // to fire. "pending" still lets the gate resolve a verdict without smuggling in validation evidence. - const liveCiSpy = vi.spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl").mockResolvedValue({ - ciState: "pending", - hasPending: true, - hasVisiblePending: true, - hasMissingRequiredContext: false, - failingDetails: [], - nonRequiredFailingDetails: [], - ciCompletenessWarning: null, - }); - const posted = { count: 0, body: "" }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") - return Response.json([ - { uid: 9, githubUsername: "contributor", githubId: "321", totalPrs: 5, totalMergedPrs: 4, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, - ]); - if (url === "https://api.gittensor.io/miners/321") return Response.json({ repositories: [{ repositoryFullName: repoFullName, totalPrs: "5", totalMergedPrs: "4", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); - if (url === "https://api.gittensor.io/miners/321/prs") return Response.json([]); - if (url === "https://mirror.gittensor.io/api/v1/miners/321/issues") return Response.json({ issues: [] }); - if (url.endsWith("/users/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); - if (url.includes("/users/contributor/repos")) return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/6012/files")) return Response.json([{ filename: "src/checkout.ts", additions: 3, deletions: 0, status: "modified" }]); - if (/\/pulls\/6012(?:\?|$)/.test(url)) return Response.json({ number: 6012, mergeable_state: "clean" }); - // Gate check-run — must succeed so gateEvaluation is produced and the unified-renderer branch runs. - if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 950 }, { status: 201 }); - if (url.includes("/check-runs/950") && method === "PATCH") return Response.json({ id: 950 }); - // Stateful comment store (mirrors the retrigger tests' own GET-finds-the-prior-POST pattern): the - // FIRST GET finds nothing (posts a fresh comment), every SUBSequent GET/PATCH finds and updates the - // SAME row -- a stub that always returns [] on GET would make the code re-POST on every update - // attempt instead of PATCHing, inflating posted.count for reasons unrelated to this test. - if (url.includes(`/issues/6012/comments`) && method === "GET") { - return Response.json(posted.count > 0 ? [{ id: 60120, body: posted.body, user: { login: "gittensory[bot]", type: "Bot" } }] : []); - } - if (url.includes(`/issues/6012/comments`) && method === "POST") { - posted.count += 1; - posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 60120 }, { status: 201 }); - } - if (url.includes(`/issues/comments/60120`) && method === "PATCH") { - posted.body = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); - return Response.json({ id: 60120 }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "checkbox-4589-full-panel", - eventName: "pull_request", - payload: { - action: "opened", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, - events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], - }, - repository: { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 6012, title: "Add retry to checkout", state: "open", user: { login: "contributor" }, head: { sha: "checkbox-4589-full-panel-sha" }, labels: [], body: "No validation evidence mentioned here." }, - }, - } as unknown as Parameters[1]); - - expect(liveCiSpy).toHaveBeenCalled(); - expect(posted.count).toBeGreaterThan(0); - expect(posted.body).toContain("
Test coverage"); - expect(posted.body).toContain("No changed test files or passing validation evidence were detected for this PR."); - expect(posted.body).toContain("- [ ] **[BETA]** Generate an AI Playwright test for this PR"); - }); - - it("handles a sparse payload with no repository, sender, or issue without throwing", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "checkbox-4589-sparse", - eventName: "issue_comment", - payload: { - action: "edited", - comment: { id: 999, body: CHECKED_GENERATE_TESTS_PANEL, user: { login: "gittensory[bot]", type: "Bot" } }, - sender: undefined, - }, - } as unknown as Parameters[1]), - ).resolves.not.toThrow(); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("missing_repo_pr_or_installation"); - }); - - it("treats a non-Bot sender whose login merely ends in '[bot]' as a bot author (spoofing guard)", async () => { - const repoFullName = "JSONbored/checkbox-4589-bot-suffix"; - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedCheckboxPr(env, repoFullName, 6013, "checkbox-4589-bot-suffix-sha"); - const posted = { count: 0, body: "" }; - stubCheckboxFetch(6013, "impersonator[bot]", "admin", posted); - - await processJob(env, checkboxWebhook(repoFullName, 6013, 911, { login: "impersonator[bot]", type: "User" })); - - expect(posted.count).toBe(0); - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.e2e_tests_generation_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("bot_author"); - }); - }); - - it("ops-alerts job no-ops when GITTENSORY_REVIEW_OPS is OFF (does no anomaly scan)", async () => { - const env = createTestEnv(); // flag unset → OFF - await env.DB.prepare("INSERT INTO repositories (full_name, owner, name, is_installed, is_registered) VALUES (?, ?, ?, 1, 1)") - .bind("owner/repo", "owner", "repo") - .run(); - // Seed a gate false-positive anomaly that WOULD fire if the scan ran. - for (let i = 1; i <= 6; i += 1) { - await recordGateBlockOutcome(env, { repoFullName: "owner/repo", pullNumber: i, blockerCodes: ["missing_linked_issue"] }); - await upsertPullRequestFromGitHub(env, "owner/repo", { number: i, title: `PR ${i}`, state: "closed", merged_at: i <= 4 ? "2026-06-01T00:00:00.000Z" : null } as never); - } - const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); - await processJob(env, { type: "ops-alerts", requestedBy: "test" }); - expect(warn.mock.calls.map((c) => String(c[0])).some((line) => line.includes("ops_anomaly"))).toBe(false); - warn.mockRestore(); - }); - - it("ops-alerts job runs the anomaly scan when GITTENSORY_REVIEW_OPS is ON", async () => { - const env = createTestEnv({ GITTENSORY_REVIEW_OPS: "true" }); - await env.DB.prepare("INSERT INTO repositories (full_name, owner, name, is_installed, is_registered) VALUES (?, ?, ?, 1, 1)") - .bind("owner/repo", "owner", "repo") - .run(); - for (let i = 1; i <= 6; i += 1) { - await recordGateBlockOutcome(env, { repoFullName: "owner/repo", pullNumber: i, blockerCodes: ["missing_linked_issue"] }); - await upsertPullRequestFromGitHub(env, "owner/repo", { number: i, title: `PR ${i}`, state: "closed", merged_at: i <= 4 ? "2026-06-01T00:00:00.000Z" : null } as never); - } - const errors = vi.spyOn(console, "error").mockImplementation(() => {}); - await processJob(env, { type: "ops-alerts", requestedBy: "test" }); - expect(errors.mock.calls.map((c) => String(c[0])).some((line) => line.includes("ops_anomaly") && line.includes("owner/repo"))).toBe(true); - errors.mockRestore(); - }); - - it("sweep-liveness-watchdog job no-ops when GITTENSORY_SWEEP_WATCHDOG is OFF (does no scan, no re-enqueue)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); // flag unset → OFF - await upsertRepositoryFromGitHub(env, { name: "stale-repo", full_name: "owner/stale-repo", private: false, owner: { login: "owner" } }, 9310); - await upsertRepositorySettings(env, { repoFullName: "owner/stale-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/stale-repo", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); - - await processJob(env, { type: "sweep-liveness-watchdog", requestedBy: "test" }); - - expect(sent).toEqual([]); - }); - - it("sweep-liveness-watchdog job runs the liveness scan and re-enqueues a stale repo when GITTENSORY_SWEEP_WATCHDOG is ON", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ GITTENSORY_SWEEP_WATCHDOG: "true", JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertRepositoryFromGitHub(env, { name: "stale-repo", full_name: "owner/stale-repo", private: false, owner: { login: "owner" } }, 9311); - await upsertRepositorySettings(env, { repoFullName: "owner/stale-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/stale-repo", { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "a1" }, labels: [], body: "" }); - - await processJob(env, { type: "sweep-liveness-watchdog", requestedBy: "test" }); - - expect(sent).toEqual([expect.objectContaining({ type: "agent-regate-sweep", repoFullName: "owner/stale-repo", installationId: 9311 })]); - }); - - it("reconcile-open-prs job no-ops when GITTENSORY_PR_RECONCILIATION is OFF (does no scan)", async () => { - const env = createTestEnv(); // flag unset → OFF - await upsertRepositoryFromGitHub(env, { name: "stale-repo", full_name: "owner/stale-repo", private: false, owner: { login: "owner" } }, 9410); - await upsertRepositorySettings(env, { repoFullName: "owner/stale-repo", autonomy: { merge: "auto" } }); - const reconcileSpy = vi.spyOn(backfillModule, "reconcileOpenPullRequests"); - - await processJob(env, { type: "reconcile-open-prs", requestedBy: "test" }); - - expect(reconcileSpy).not.toHaveBeenCalled(); - }); - - it("reconcile-open-prs job runs the reconciliation scan when GITTENSORY_PR_RECONCILIATION is ON", async () => { - const env = createTestEnv({ GITTENSORY_PR_RECONCILIATION: "true" }); - await upsertRepositoryFromGitHub(env, { name: "stale-repo", full_name: "owner/stale-repo", private: false, owner: { login: "owner" } }, 9411); - await upsertRepositorySettings(env, { repoFullName: "owner/stale-repo", autonomy: { merge: "auto" } }); - const reconcileSpy = vi.spyOn(backfillModule, "reconcileOpenPullRequests").mockResolvedValue({ repoFullName: "owner/stale-repo", remoteOpenCount: 0, localOpenCount: 0, missingNumbers: [] }); - - await processJob(env, { type: "reconcile-open-prs", requestedBy: "test" }); - - expect(reconcileSpy).toHaveBeenCalledWith(env, "owner/stale-repo"); - reconcileSpy.mockRestore(); - }); - - describe("type label decoupling (#label-decoupling)", () => { - function stubTypeLabelFetch(prNumber: number, seen: { posted: string[]; removed: string[]; checkRunCreated: boolean }) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes(`/commits/`) && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - seen.checkRunCreated = true; - return Response.json({ id: 9001 }, { status: 201 }); - } - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 9001 }); - if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { - seen.posted.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.includes(`/issues/${prNumber}/labels/`) && method === "DELETE") { - seen.removed.push(decodeURIComponent(url.split(`/issues/${prNumber}/labels/`)[1] ?? "")); - return new Response(null, { status: 204 }); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); - if (url.includes(`/issues/${prNumber}/comments`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/comments`) && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - } - - // Fails only the ONE audit_events insert whose bound values include `needle` (e.g. a specific - // eventType), leaving every other audit write in the same job untouched -- a blanket "throw on any - // audit_events insert" (as the sibling #orb-ci-stuck-repeat fail-open tests use for a narrower job - // type) breaks unrelated earlier writes on the fuller pull_request webhook path used here. - function failAuditEventInsertsContaining(env: Env, needle: string) { - const realPrepare = env.DB.prepare.bind(env.DB); - env.DB.prepare = ((sql: string) => { - const statement = realPrepare(sql); - if (!/insert\s+into\s+["`]?audit_events["`]?/i.test(sql)) return statement; - return { - ...statement, - bind(...values: unknown[]) { - const bound = statement.bind(...(values as never[])); - if (!values.some((value) => typeof value === "string" && value.includes(needle))) return bound; - return { ...bound, run: () => Promise.reject(new Error("audit write failed")) }; - }, - }; - }) as typeof env.DB.prepare; - } - - it("applies the type label when oss_maintainer mode + an unconfirmed miner suppress the context label", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - publicAudienceMode: "oss_maintainer", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "not_found" }, 60_000); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(210, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-oss-maintainer-unconfirmed", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 210, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha210" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.posted).toEqual(["gittensor:bug"]); - expect(seen.removed.sort()).toEqual(["gittensor:feature", "gittensor:priority"]); - }); - - it("keeps gate-only gittensor_only type labels silent until miner confirmation", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - publicAudienceMode: "gittensor_only", - autoLabelEnabled: false, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false, minerList: 0 }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") { - seen.minerList += 1; - return Response.json([]); - } - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes(`/commits/`) && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") { - seen.checkRunCreated = true; - return Response.json({ id: 9002 }, { status: 201 }); - } - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 9002 }); - if (url.includes(`/issues/218/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/218/labels`) && method === "POST") { - seen.posted.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.includes(`/issues/218/labels/`) && method === "DELETE") { - seen.removed.push(decodeURIComponent(url.split(`/issues/218/labels/`)[1] ?? "")); - return new Response(null, { status: 204 }); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-gittensor-only-gate-only-muted", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 218, title: "fix: gate-only silence", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha218" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.minerList).toBe(1); - expect(seen.checkRunCreated).toBe(true); - expect(seen.posted).toEqual([]); - expect(seen.removed).toEqual([]); - }); - - it("still mutes the type label when gittensor_only mode's non-confirmed-miner silence applies, even with the gate enabled", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "all_prs", - publicSurface: "comment_and_label", - publicAudienceMode: "gittensor_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - // The only difference from the pre-existing "keeps GitHub-history-only contributors quiet" test - // (which has the gate off, so it returns before ever reaching the type-label decision): with the - // gate ENABLED, the function does NOT bail out early, so this is the only path that actually - // exercises `decision.skipReason === "not_official_gittensor_miner"` at the type-label gate. - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "not_found" }, 60_000); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(217, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-gittensor-only-muted", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 217, title: "fix: gittensor_only silence", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha217" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.posted).toEqual([]); - expect(seen.removed).toEqual([]); - }); - - it("does not apply the type label when typeLabelsEnabled is false, in the same oss_maintainer + unconfirmed-miner scenario", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - publicAudienceMode: "oss_maintainer", - autoLabelEnabled: true, - typeLabelsEnabled: false, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "not_found" }, 60_000); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(211, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-disabled-oss-maintainer-unconfirmed", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 211, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha211" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.posted).toEqual([]); - expect(seen.removed).toEqual([]); - }); - - it("applies the type label to a maintainer-authored PR even though includeMaintainerAuthors excludes it from the public surface", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - includeMaintainerAuthors: false, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(212, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-maintainer-author", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 212, title: "fix: internal cleanup", state: "open", user: { login: "org-member" }, author_association: "MEMBER", head: { sha: "sha212" }, labels: [], body: "Internal." }, - }, - }); - - expect(seen.posted).toEqual(["gittensor:bug"]); - expect(seen.posted).not.toContain("gittensor"); - }); - - it("applies the type label to a bot-authored PR and keeps the three type labels mutually exclusive", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(213, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-bot-author", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 213, title: "feat: add retry backoff", state: "open", user: { login: "renovate[bot]", type: "Bot" }, head: { sha: "sha213" }, labels: [], body: "Automated." }, - }, - }); - - expect(seen.posted).toEqual(["gittensor:feature"]); - expect(seen.removed.sort()).toEqual(["gittensor:bug", "gittensor:priority"]); - }); - - it("cleans up an arbitrary configured custom category alongside bug/feature/priority (#label-modularity)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - // A self-host taxonomy well beyond the built-in bug/feature/priority triad (#label-modularity): - // `security` is a registered category with no title-classification rule of its own, so it is - // never CHOSEN here, but it must still be a cleanup CANDIDATE (never left dangling on a PR whose - // classification moved elsewhere) exactly like the built-in categories. - typeLabels: { bug: "gittensor:bug", feature: "gittensor:feature", priority: "gittensor:priority", security: "area:security" }, - }); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(218, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-custom-category-cleanup", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 218, title: "feat: add retry backoff", state: "open", user: { login: "renovate[bot]", type: "Bot" }, head: { sha: "sha218" }, labels: [], body: "Automated." }, - }, - }); - - expect(seen.posted).toEqual(["gittensor:feature"]); - expect(seen.removed.sort()).toEqual(["area:security", "gittensor:bug", "gittensor:priority"]); - }); - - it("applies the type label when publicSurface: comment_only makes the base context label structurally impossible", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "comment_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(214, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-comment-only-surface", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 214, title: "fix: comment-only regression", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha214" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.posted).toEqual(["gittensor:bug"]); - }); - - it("typeLabelsEnabled: false does not suppress the base context label for a confirmed contributor", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - typeLabelsEnabled: false, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(215, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-disabled-confirmed-contributor", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 215, title: "fix: confirmed contributor path", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha215" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.posted).toEqual(["gittensor"]); - }); - - it("posts the Gittensory Context check run independently of both label families being off, with zero label writes", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - typeLabelsEnabled: false, - checkRunMode: "enabled", - gateCheckMode: "off", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(216, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-checkrun-independent", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 216, title: "fix: check-run independence", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha216" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.checkRunCreated).toBe(true); - expect(seen.posted).toEqual([]); - expect(seen.removed).toEqual([]); - }); - - function stubPropagationFetch( - prNumber: number, - linkedIssueNumber: number, - seen: { posted: string[]; removed: string[]; issueFetches: number }, - linkedIssueResponse: () => Response, - ) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes(`/commits/`) && url.includes("/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/check-runs") && method === "POST") return Response.json({ id: 9001 }, { status: 201 }); - if (url.includes("/check-runs/") && method === "PATCH") return Response.json({ id: 9001 }); - if (url.endsWith(`/issues/${linkedIssueNumber}`) && method === "GET") { - seen.issueFetches += 1; - return linkedIssueResponse(); - } - if (url.includes(`/issues/${prNumber}/labels`) && method === "GET") return Response.json([]); - if (url.includes(`/issues/${prNumber}/labels`) && method === "POST") { - seen.posted.push(...((JSON.parse(String(init?.body ?? "{}")).labels ?? []) as string[])); - return Response.json([]); - } - if (url.includes(`/issues/${prNumber}/labels/`) && method === "DELETE") { - seen.removed.push(decodeURIComponent(url.split(`/issues/${prNumber}/labels/`)[1] ?? "")); - return new Response(null, { status: 204 }); - } - if (url.endsWith("/labels") && method === "POST") return Response.json({ name: JSON.parse(String(init?.body ?? "{}")).name }, { status: 201 }); - if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - } - - it("applies the configured priority label when a linked issue already carries the configured issue label (#priority-linked-issue-gate)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - linkedIssueLabelPropagation: { - enabled: true, - mode: "exclusive_type_label", - mappings: [{ issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: true }], - }, - }); - const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; - stubPropagationFetch(220, 1, seen, () => Response.json({ number: 1, state: "open", user: { login: "contributor" }, labels: ["gittensor:priority"] })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "priority-propagation-applied", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 220, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha220" }, labels: [], body: "Fixes #1" }, - }, - }); - - // JSONbored/gittensory falls back to its own bundled manifest (GITTENSORY_REPO_FOCUS_MANIFEST_YAML) when - // no other manifest source responds, which REPLACES this test's DB-configured single-mapping override - // with its own bug/feature (exclusive) + priority (additive) mapping list -- so the linked issue's - // gittensor:priority label composes with the title-derived "fix" -> gittensor:bug, rather than replacing - // it. Priority is additive (not a type of its own; see resolvePrTypeLabel's composition fix), so bug - // still applies from the title and only feature (never matched) needs removing. - expect(seen.issueFetches).toBe(1); - expect(seen.posted).toEqual(["gittensor:bug", "gittensor:priority"]); - expect(seen.removed).toEqual(["gittensor:feature"]); - }); - - it("REGRESSION (#4528, PR #4494 shape): keeps the propagated labels on the PR's own merge-closed webhook, instead of falling back to the title guess", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "acme/widget", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "off", - reviewCheckMode: "disabled", - linkedIssueGateMode: "off", - aiReviewMode: "off", - // Real-world shape: the type-label decision runs regardless of the check-run/gate publish mode, but - // the SURROUNDING function only reaches that far for an already-closed PR when the agent layer is - // configured (autonomyNeedsGateEvaluation) -- an unconfigured repo's closed-PR pass has nothing else - // to do and bails before the label block. `label: "auto"` is the minimal opt-in that reproduces this - // without pulling in merge/close autonomy's own CI-wait/rebase machinery. - autonomy: { label: "auto" }, - linkedIssueLabelPropagation: { - enabled: true, - mode: "exclusive_type_label", - mappings: [ - { issueLabel: "gittensor:feature", prLabel: "gittensor:feature", removeOtherTypeLabels: true }, - { issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: false }, - ], - }, - }); - const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; - // The linked issue is CLOSED, at a timestamp at/after this PR's own merge -- GitHub's standard "Closes #N" - // auto-close, fired by this very merge. Title deliberately uses a verb ("fold") absent from the - // feature-action-verb whitelist, so a title-only fallback would misclassify this as gittensor:bug -- - // this only stays gittensor:feature/gittensor:priority if the merge-closed issue is still trusted. - stubPropagationFetch(4494, 4279, seen, () => - Response.json({ - number: 4279, - state: "closed", - closed_at: "2026-07-09T22:15:14Z", - user: { login: "contributor" }, - labels: ["gittensor:feature", "gittensor:priority"], - }), - ); - - await processJob(env, { - type: "github-webhook", - deliveryId: "merge-close-race-4528", - eventName: "pull_request", - payload: { - action: "closed", - installation: { id: 123, account: { login: "acme", id: 1, type: "User" } }, - repository: { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, - pull_request: { - number: 4494, - title: "feat(x): fold run-state into the status panel", - state: "closed", - merged_at: "2026-07-09T22:15:13Z", - user: { login: "contributor" }, - author_association: "NONE", - head: { sha: "sha4494" }, - labels: [], - body: "Closes #4279", - }, - }, - }); - - expect(seen.issueFetches).toBe(1); - expect(seen.posted.sort()).toEqual(["gittensor:feature", "gittensor:priority"]); - expect(seen.removed).toEqual(["gittensor:bug"]); - }); - - it("REGRESSION (#regression-safe-propagation, was: 'fails open to the normal title-based label'): skips the label decision entirely — never falls back to title — when the linked issue's fetch fails, leaving existing labels untouched", async () => { - // Before the fix, a fetch failure here fell through to the title guess and OVERWROTE whatever labels - // were already correct — the exact mechanism (an inconclusive recheck treated as a confirmed absence - // of propagation authority) that let a transient GitHub hiccup permanently strip a correctly propagated - // gittensor:feature/gittensor:priority label down to gittensor:bug (confirmed in production, PRs - // #4716/#4783 and 116 others in a 2-day sample). A fetch failure must now be a no-op, not a downgrade. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - linkedIssueLabelPropagation: { - enabled: true, - mode: "exclusive_type_label", - mappings: [{ issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: true }], - }, - }); - const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; - stubPropagationFetch(221, 1, seen, () => new Response("server error", { status: 500 })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "priority-propagation-fetch-failed", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 221, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha221" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.issueFetches).toBe(1); - expect(seen.posted).toEqual([]); - expect(seen.removed).toEqual([]); - const events = await env.DB.prepare( - `select outcome, detail from audit_events where event_type = 'github_app.type_label_decision' and target_key = 'JSONbored/gittensory#221'`, - ).all(); - expect(events.results).toEqual([{ outcome: "denied", detail: "propagation_inconclusive" }]); - }); - - it("REGRESSION (#regression-safe-propagation): a second pass whose propagation recheck is inconclusive never clobbers a first pass's already-correct propagated labels", async () => { - // Reproduces the exact PR #4716/#4783 shape end-to-end: an EARLIER pass correctly propagates - // gittensor:feature/gittensor:priority from the linked issue, then a LATER pass (a webhook re-review, a - // sweep tick, or simply a second near-simultaneous delivery for the same merge) re-runs the same - // decision but this time the linked issue's fetch fails transiently. The later pass must leave the - // correct labels exactly as the first pass left them. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "acme/widget", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - linkedIssueLabelPropagation: { - enabled: true, - mode: "exclusive_type_label", - mappings: [{ issueLabel: "gittensor:feature", prLabel: "gittensor:feature", removeOtherTypeLabels: true }], - }, - }); - const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; - let issueShouldFail = false; - stubPropagationFetch(4716, 2216, seen, () => - issueShouldFail - ? new Response("server error", { status: 500 }) - : Response.json({ number: 2216, state: "open", user: { login: "contributor" }, labels: ["gittensor:feature"] }), - ); - // Each pass uses a DIFFERENT action/head SHA so the second is a genuinely fresh re-evaluation, not a - // same-head no-op the surface-publish guard would short-circuit before ever reaching the label block. - const webhookPayload = (action: "opened" | "synchronize", headSha: string) => ({ - action, - installation: { id: 123, account: { login: "acme", id: 1, type: "User" as const } }, - repository: { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, - pull_request: { number: 4716, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE" as const, head: { sha: headSha }, labels: [], body: "Closes #2216" }, - }); - - await processJob(env, { type: "github-webhook", deliveryId: "pass-1-correct", eventName: "pull_request", payload: webhookPayload("opened", "sha4716a") }); - expect(seen.posted).toEqual(["gittensor:feature"]); - expect(seen.removed.sort()).toEqual(["gittensor:bug", "gittensor:priority"]); - - issueShouldFail = true; - await processJob(env, { type: "github-webhook", deliveryId: "pass-2-inconclusive", eventName: "pull_request", payload: webhookPayload("synchronize", "sha4716b") }); - // No FURTHER posts/removes happened in pass 2 -- the correct labels from pass 1 are exactly as they were. - expect(seen.posted).toEqual(["gittensor:feature"]); - expect(seen.removed.sort()).toEqual(["gittensor:bug", "gittensor:priority"]); - }); - - it("REGRESSION (#regression-safe-propagation): a contended per-PR actuation lock skips the label decision entirely instead of racing the pass that already holds it", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - linkedIssueLabelPropagation: { - enabled: true, - mode: "exclusive_type_label", - mappings: [{ issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: true }], - }, - }); - const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; - stubPropagationFetch(223, 1, seen, () => Response.json({ number: 1, state: "open", user: { login: "contributor" }, labels: ["gittensor:priority"] })); - - // Simulates a concurrent pass (a sibling webhook delivery, or the sweep) already holding this exact - // PR's actuation lock when this pass reaches the type-label block. - const held = await claimPrActuationLock(env, "JSONbored/gittensory", 223); - expect(held.acquired).toBe(true); - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "priority-propagation-lock-contended", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 223, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha223" }, labels: [], body: "Fixes #1" }, - }, - }); - } finally { - await releasePrActuationLock(env, "JSONbored/gittensory", 223, held.ownerToken); - } - - // The fetch never even reaches the linked-issue check -- the lock is claimed BEFORE any propagation work. - expect(seen.issueFetches).toBe(0); - expect(seen.posted).toEqual([]); - expect(seen.removed).toEqual([]); - const events = await env.DB.prepare( - `select outcome, detail from audit_events where event_type = 'github_app.type_label_decision' and target_key = 'JSONbored/gittensory#223'`, - ).all(); - expect(events.results).toEqual([{ outcome: "denied", detail: "lock_contended" }]); - }); - - it("never fetches a linked issue and keeps normal behavior when propagation is left at its default (disabled) (#priority-linked-issue-gate)", async () => { - // Deliberately NOT "JSONbored/gittensory" (unlike its two sibling tests above): this repo's own - // `.gittensory.yml` now enables propagation for itself (#priority-linked-issue-gate-ownership - // dogfooding), and `resolveRepositorySettings` falls back to the bundled - // `GITTENSORY_REPO_FOCUS_MANIFEST_YAML` copy of it whenever a live manifest fetch is unavailable - // (`isGittensorySelfRepo`, `src/signals/focus-manifest-loader.ts`) -- exactly the case in this test's - // stubbed fetch. Using gittensory's own literal repo name here would make this "propagation is off by - // DEFAULT" test silently stop being a default-behavior test at all. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "acme/widget", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - // linkedIssueLabelPropagation intentionally omitted -- defaults to disabled. - }); - const seen = { posted: [] as string[], removed: [] as string[], issueFetches: 0 }; - stubPropagationFetch(222, 1, seen, () => Response.json({ number: 1, state: "open", labels: ["gittensor:priority"] })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "priority-propagation-disabled-noop", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "acme", id: 1, type: "User" } }, - repository: { name: "widget", full_name: "acme/widget", private: false, owner: { login: "acme" } }, - pull_request: { number: 222, title: "fix: some bug", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha222" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.issueFetches).toBe(0); - expect(seen.posted).toEqual(["gittensor:bug"]); - expect(seen.removed.sort()).toEqual(["gittensor:feature", "gittensor:priority"]); - }); - - it("records the audit event for a normal applied label decision (#label-decoupling audit)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(219, seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-recorded", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 219, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha219" }, labels: [], body: "Fixes #1" }, - }, - }); - - expect(seen.posted).toEqual(["gittensor:bug"]); - const labelEvent = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?") - .bind("github_app.type_label_decision", "JSONbored/gittensory#219") - .first<{ outcome: string; detail: string }>(); - expect(labelEvent?.outcome).toBe("completed"); - expect(labelEvent?.detail).toBe("applied labels: gittensor:bug"); - }); - - it("does not let a failing audit write stop label application (completed outcome, fail-open)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "label_only", - autoLabelEnabled: true, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(220, seen); - failAuditEventInsertsContaining(env, "github_app.type_label_decision"); - - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-completed-audit-fail", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 220, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha220" }, labels: [], body: "Fixes #1" }, - }, - }); - - // The label application itself must complete even though its audit-event write threw. - expect(seen.posted).toEqual(["gittensor:bug"]); - }); - - it("does not let a failing audit write stop the decision when type labels are disabled (denied outcome, fail-open)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: true, - typeLabelsEnabled: false, - createMissingLabel: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - const seen = { posted: [] as string[], removed: [] as string[], checkRunCreated: false }; - stubTypeLabelFetch(221, seen); - failAuditEventInsertsContaining(env, "github_app.type_label_decision"); - - // Fail-open: the webhook job must still complete (and still reach the type-label decision) even - // though recording it fails. - await processJob(env, { - type: "github-webhook", - deliveryId: "type-label-denied-audit-fail", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 221, title: "fix: broken pagination", state: "open", user: { login: "contributor" }, author_association: "NONE", head: { sha: "sha221" }, labels: [], body: "Fixes #1" }, - }, - }); - expect(seen.posted).toEqual([]); - }); - }); -}); - -function completeSegment(repoFullName: string, segment: "labels" | "open_issues" | "open_pull_requests") { - return { - repoFullName, - segment, - status: "complete" as const, - sourceKind: "test" as const, - mode: "resume" as const, - fetchedCount: 1, - expectedCount: 1, - pageCount: 1, - completedAt: "2026-05-25T00:00:00.000Z", - warnings: [], - }; -} - -type CommandAnswerFixture = Parameters[1]; - -function commandAnswer(id: string, command: string, overrides: Partial = {}): CommandAnswerFixture { - return { - id, - repoFullName: "JSONbored/gittensory", - issueNumber: 77, - command, - requestCommentId: 7, - responseCommentId: 9001, - responseUrl: "https://github.com/JSONbored/gittensory/pull/77#issuecomment-9001", - actorKind: "maintainer" as const, - createdAt: "2026-05-28T00:00:00.000Z", - updatedAt: "2026-05-28T00:00:00.000Z", - metadata: {}, - ...overrides, - }; -} - -function commandAnswerBody(answerId: string, command: string): string { - return [ - "", - ``, - `Command: \`@gittensory ${command}\``, - "Feedback is aggregate-only.", - ].join("\n"); -} - -function queueMinerSnapshot(login: string) { - return { - source: "gittensor_api" as const, - githubId: "123", - githubUsername: login, - isEligible: true, - credibility: 1, - eligibleRepoCount: 1, - issueDiscoveryScore: 0, - issueTokenScore: 0, - issueCredibility: 1, - isIssueEligible: false, - issueEligibleRepoCount: 0, - alphaPerDay: 0, - taoPerDay: 0, - usdPerDay: 0, - totals: { - pullRequests: 3, - mergedPullRequests: 2, - openPullRequests: 1, - closedPullRequests: 0, - openIssues: 0, - closedIssues: 0, - solvedIssues: 0, - validSolvedIssues: 0, - }, - repositories: [], - pullRequests: [], - issueLabels: [], - }; -} - -function b64(value: string): string { - return Buffer.from(value, "utf8").toString("base64"); -} - -function withProductUsageInsertFailure(env: Env): Env { - const db = env.DB as unknown as { prepare(sql: string): unknown; batch(statements: unknown[]): Promise }; - return { - ...env, - DB: { - prepare(sql: string) { - if (sql.includes("product_usage_events")) throw new Error("product usage insert failed"); - return db.prepare.call(db, sql); - }, - batch(statements: unknown[]) { - return db.batch.call(db, statements); - }, - } as unknown as D1Database, - }; -} - -async function generatePrivateKeyPem(): Promise { - const key = (await crypto.subtle.generateKey( - { - name: "RSASSA-PKCS1-v1_5", - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }, - true, - ["sign", "verify"], - )) as CryptoKeyPair; - const exported = await crypto.subtle.exportKey("pkcs8", key.privateKey); - const base64 = Buffer.from(exported as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); - return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`; -} - -describe("changedPathsForGuardrail", () => { - it("collects current + rename paths and skips empty entries", () => { - const files = [ - { path: "src/a.ts", previousFilename: null }, - { path: "src/b.ts", previousFilename: "src/old-b.ts" }, // a rename contributes both names - { path: "", previousFilename: "" }, // an empty path AND empty rename are both skipped (both guard branches false) - ] as unknown as Parameters[0]; - expect(changedPathsForGuardrail(files)).toEqual(["src/a.ts", "src/b.ts", "src/old-b.ts"]); - }); -}); - -describe("agentMaintenanceHeadMatchesGate", () => { - it("allows maintenance only when the stored PR head still matches the reviewed gate head", () => { - expect(agentMaintenanceHeadMatchesGate("reviewed", "reviewed")).toBe(true); - expect(agentMaintenanceHeadMatchesGate("reviewed", "new-unreviewed")).toBe(false); - }); - - it("keeps legacy no-SHA paths fail-open because no exact reviewed head can be pinned", () => { - expect(agentMaintenanceHeadMatchesGate(undefined, "current")).toBe(true); - expect(agentMaintenanceHeadMatchesGate("reviewed", null)).toBe(true); - }); - - it("REGRESSION (#stale-head): a newer synchronize that advances the stored head before maintenance acts blocks the stale-gate auto-merge", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { - action: "created", - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - target_type: "User", - repository_selection: "all", - permissions: { metadata: "read", contents: "write", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - // Clean, mergeable, approved, green CI + merge:auto + approve:auto + close:auto — this PR WOULD be auto-acted. - // The ONLY thing that must stop it is the stale-head guard in maybeRunAgentMaintenance. - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { merge: "auto", approve: "auto", close: "auto" }, - }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/commits/stale1/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/stale1/status")) return Response.json({ statuses: [] }); - if (url.includes("/check-runs")) return Response.json({ id: 902 }, { status: 201 }); - return new Response("not found", { status: 404 }); - }); - - // Simulate the concurrent-queue race the guard defends against: the gate evaluated head "stale1", but by the - // time maintenance re-reads the persisted row a newer `synchronize` has advanced the stored head to "newer2". - // maybeRunAgentMaintenance re-reads via getPullRequest, so divert that read to the advanced head. - const realGetPullRequest = repositoriesModule.getPullRequest; - const spy = vi.spyOn(repositoriesModule, "getPullRequest").mockImplementation(async (...callArgs) => { - const row = await realGetPullRequest(...callArgs); - return row ? { ...row, headSha: "newer2" } : row; - }); - try { - await processJob(env, { - type: "github-webhook", - deliveryId: "stale-head-no-maintenance", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 71, title: "Clean PR", state: "open", user: { login: "contributor" }, head: { sha: "stale1" }, labels: [], body: "Closes #1", mergeable_state: "clean", reviewDecision: "APPROVED" }, - }, - }); - } finally { - spy.mockRestore(); - } - - // No terminal maintenance action of ANY class fires: the gate verdict belonged to the now-stale head. - const acted = await env.DB.prepare("select count(*) as n from audit_events where event_type in ('agent.action.merge','agent.action.approve','agent.action.close')").first<{ n: number }>(); - expect(acted?.n).toBe(0); - }); -}); - -describe("one-shot reopen prevention", () => { - beforeEach(() => { - clearInstallationTokenCacheForTest(); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - it("re-closes contributor reopens after a write collaborator closed the PR", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - // Both a "closed" event (by the write collaborator) AND a "reopened" event (by the contributor, still the - // most recent reopener) — the new live re-check (#2369) reads this same endpoint to confirm the contributor - // is still the current reopener before proceeding to close. - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); // opted into acting autonomy - - await processJob(env, { - type: "github-webhook", - deliveryId: "reopen-write-collab-close", - eventName: "pull_request", - payload: reopenedPayload("contributor"), - }); - - expect(calls.some((call) => call.url.endsWith("/collaborators/contributor/permission"))).toBe(true); - expect(calls.some((call) => call.url.endsWith("/collaborators/maintainer/permission"))).toBe(true); - expect(calls.some((call) => call.method === "POST" && call.url.endsWith("/issues/42/comments"))).toBe(true); - expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(true); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); // #2260: a successful close is unaffected - expect(audit?.detail).toContain("originally closed by maintainer"); - // #review-audit: the early return after a re-close stamps the delivery processed (was left "queued"). - const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("reopen-write-collab-close").first<{ status: string }>(); - expect(webhookRow?.status).toBe("processed"); - }); - - it("does NOT re-close a disallowed reopen when live PR state has moved since the webhook was received (#2130, #2261)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - // A maintainer legitimately reopened/re-approved the PR — or a queue retry replayed a stale payload — in - // the window between the original webhook delivery and this handler's permission/closer-history reads. The - // live re-check must catch it and deny the re-close rather than overwriting a live maintainer decision. - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "head_changed", expectedHeadSha: "abc123", liveHeadSha: "def456", liveState: "open" }); - - await processJob(env, { type: "github-webhook", deliveryId: "reopen-stale", eventName: "pull_request", payload: reopenedPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("reopen re-close not executed"); - }); - - it("REGRESSION: does NOT re-close when the reopener gained maintainer permission before the close fires (#2130 follow-up)", async () => { - // Same head, still open — a head/state-only freshness check would say "current". But the reopener could - // have been promoted to a write/maintain/admin collaborator (or added as one) in the window between the - // initial permission read and this handler's close, which retroactively authorizes exactly the reopen - // this handler is about to undo. - const calls: Array<{ url: string; method: string }> = []; - let contributorPermissionCalls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) { - contributorPermissionCalls += 1; - // First read (upstream decision to re-close at all): still just a reader. Second read (the live - // re-check right before the mutation): promoted to a write collaborator. - return Response.json({ permission: contributorPermissionCalls === 1 ? "read" : "write" }); - } - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - - await processJob(env, { type: "github-webhook", deliveryId: "reopen-promoted", eventName: "pull_request", payload: reopenedPayload("contributor") }); - - expect(contributorPermissionCalls).toBe(2); - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("now holds maintainer permission"); - }); - - it("REGRESSION: does NOT re-close when a DIFFERENT (maintainer) reopener supersedes the original disallowed reopen (#2369)", async () => { - // The original contributor reopen is what triggered this handler, but by the time it runs, a real maintainer - // has ALSO reopened the same PR (a legitimate, authorized reopen is now the current reason it's open). Head/ - // state freshness and the reopener's OWN permission re-check both miss this — neither sees WHO most recently - // reopened. The timeline shows a "closed" event by "maintainer" (the original one-shot close) followed by a - // LATER "reopened" event by a different maintainer login ("second-maintainer"), after the contributor's own - // earlier reopen. - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) { - return Response.json([ - { event: "closed", actor: { login: "maintainer" } }, - { event: "reopened", actor: { login: "contributor" } }, - { event: "closed", actor: { login: "maintainer" } }, - { event: "reopened", actor: { login: "second-maintainer" } }, - ]); - } - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - - await processJob(env, { type: "github-webhook", deliveryId: "reopen-superseded", eventName: "pull_request", payload: reopenedPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("second-maintainer"); - expect(audit?.detail).toContain("not contributor"); - }); - - it("happy path unaffected: re-closes when the same reopener is still the latest reopener on the timeline (#2369)", async () => { - // Confirms the new live re-check does not spuriously block the ordinary case: the contributor is BOTH the - // original AND the still-current reopener (no one else reopened it again in the meantime). - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - - await processJob(env, { type: "github-webhook", deliveryId: "reopen-same-latest", eventName: "pull_request", payload: reopenedPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - }); - - it("REGRESSION: denies when padding makes the latest reopener ambiguous beyond the inspected event window", async () => { - const calls: Array<{ url: string; method: string }> = []; - const eventPages: number[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.includes("/issues/42/events")) { - const page = Number(new URL(url).searchParams.get("page") ?? "1"); - eventPages.push(page); - if (page === 1) { - return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }], { - headers: { link: '; rel="last"' }, - }); - } - if (page === 12) return Response.json([{ event: "reopened", actor: { login: "second-maintainer" } }]); - return Response.json([{ event: "renamed", actor: { login: "contributor" } }]); - } - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - - await processJob(env, { type: "github-webhook", deliveryId: "reopen-window-stuffed", eventName: "pull_request", payload: reopenedPayload("contributor") }); - - expect(eventPages).toContain(22); - expect(eventPages).not.toContain(12); - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("the current reopener is now unknown, not contributor"); - }); - - it("REGRESSION: fails CLOSED (denies the re-close) when the reopener-timeline read errors (#2369)", async () => { - // The reopener-timeline lookup errors (network failure) → getLastReopenerLogin catches and returns - // { login: null, coveredAllPages: false, errored: true } — DISTINCT from the padded-window case above - // (which has errored: false). The design explicitly fails CLOSED here (deny the close) rather than - // proceeding, since wrongly re-closing a maintainer-authorized PR is worse than leaving a disallowed - // reopen open for one more tick. - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) throw new Error("GitHub events API down"); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - - await processJob(env, { type: "github-webhook", deliveryId: "reopen-timeline-error", eventName: "pull_request", payload: reopenedPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("could not confirm"); - }); - - it("REGRESSION: denies with an 'unknown' current-reopener detail when the timeline genuinely has no reopen event at all (#2369)", async () => { - // The window is FULLY covered (a single page, no Link header) but contains no "reopened" event whatsoever — - // getLastReopenerLogin returns { login: null, coveredAllPages: true }, which is NOT the ambiguous case (that - // requires coveredAllPages: false); it lands on the "superseded by a different actor" arm with a null login, - // exercising the `latestReopenerLogin ?? "unknown"` fallback in the audit detail. - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - - await processJob(env, { type: "github-webhook", deliveryId: "reopen-no-reopen-event", eventName: "pull_request", payload: reopenedPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("the current reopener is now unknown, not contributor"); - }); - - it("swallows a recordAuditEvent failure on the superseded-reopener denial path — handler still completes (#2369)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) { - return Response.json([ - { event: "closed", actor: { login: "maintainer" } }, - { event: "reopened", actor: { login: "contributor" } }, - { event: "closed", actor: { login: "maintainer" } }, - { event: "reopened", actor: { login: "second-maintainer" } }, - ]); - } - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "reopen-superseded-audit-fail", eventName: "pull_request", payload: reopenedPayload("contributor") }), - ).resolves.toBeUndefined(); - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("swallows a recordAuditEvent failure on the stale-reopen denial path — handler still completes (#2130)", async () => { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "head_changed", expectedHeadSha: "abc123", liveHeadSha: "def456", liveState: "open" }); - vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "reopen-stale-audit-fail", eventName: "pull_request", payload: reopenedPayload("contributor") }), - ).resolves.toBeUndefined(); - }); - - it("swallows a recordAuditEvent failure on the promoted-reopener denial path — handler still completes (#2130 follow-up)", async () => { - let contributorPermissionCalls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) { - contributorPermissionCalls += 1; - return Response.json({ permission: contributorPermissionCalls === 1 ? "read" : "write" }); - } - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "reopen-promoted-audit-fail", eventName: "pull_request", payload: reopenedPayload("contributor") }), - ).resolves.toBeUndefined(); - expect(contributorPermissionCalls).toBe(2); - }); - - it("records outcome:error (not completed) when the reclose PATCH call itself fails (#2260)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - // "contributor" (the payload's reopener) must be the MOST RECENT "reopened" actor in the timeline, or the - // #2369 live-recheck #3 (reopenerSuperseded) denies before ever reaching the close attempt this test targets. - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); // the courtesy comment succeeds - if (url.endsWith("/pulls/42") && method === "PATCH") return new Response("forbidden", { status: 403 }); // the close itself fails - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "reopen-close-fails", - eventName: "pull_request", - payload: reopenedPayload("contributor"), - }); - - expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(true); // the close WAS attempted - const audit = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string; metadata_json: string }>(); - expect(audit?.outcome).toBe("error"); // NOT "completed" — the close did not actually succeed - expect(audit?.detail).toContain("FAILED to re-close"); - expect(JSON.parse(audit?.metadata_json ?? "{}").error).toBeTruthy(); - // The handler still owns the decision (never falls through to normal re-review) even though the API call failed. - const webhookRow = await env.DB.prepare("select status from webhook_events where delivery_id = ?").bind("reopen-close-fails").first<{ status: string }>(); - expect(webhookRow?.status).toBe("processed"); - }); - - it("retries the reopen-reclose when a concurrent delivery already holds the per-PR actuation lock (#2447)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto" } }); - // Simulates a DIFFERENT concurrent delivery for the same PR already in flight (e.g. the draft-dodge sibling - // racing this reopen) — the lock key it would hold is pre-claimed here. - await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:jsonbored/gittensory#42", "1", 60); - // A contended lock must still stop before resolveRepositorySettings, the first call the normal re-review makes, - // but must NOT stamp this reopen delivery processed: the lock holder may be an unrelated same-PR guard that - // no-ops, so the queue needs to retry this reopen guard once the lock clears. - const resolveSettingsSpy = vi.spyOn(repositorySettingsModule, "resolveRepositorySettings"); - - await expect( - processJob(env, { - type: "github-webhook", - deliveryId: "reopen-lock-contended", - eventName: "pull_request", - payload: reopenedPayload("contributor"), - }), - ).rejects.toMatchObject({ retryKind: "pr_actuation_lock_contended" }); - - expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ n: number }>(); - expect(audit?.n).toBe(0); // no decision recorded either way — retry owns the eventual reopen decision - expect(resolveSettingsSpy).not.toHaveBeenCalled(); // the normal re-review pass never started - const webhookRow = await env.DB.prepare("select status, error_summary from webhook_events where delivery_id = ?").bind("reopen-lock-contended").first<{ status: string; error_summary: string }>(); - expect(webhookRow?.status).toBe("error"); - expect(webhookRow?.error_summary).toContain("pr actuation lock contended"); - }); - - it("does NOT re-close a disallowed reopen on an OBSERVE-only / un-opted-in repo (autonomy floor, #review-audit)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - // NO autonomy configured (observe-only / un-opted-in): the agent must take NO destructive action. - await processJob(env, { type: "github-webhook", deliveryId: "reopen-observe-only", eventName: "pull_request", payload: reopenedPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); // never commented - }); - - it("does NOT re-close a disallowed reopen while the global freeze is on — records a skip instead (#killswitch-gap)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); // opted into acting autonomy - await repositoriesModule.setGlobalAgentFrozen(env, true); // emergency brake on - await processJob(env, { type: "github-webhook", deliveryId: "reopen-frozen", eventName: "pull_request", payload: reopenedPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("skipped (agent paused)"); - }); - - it("dry-run: audits a would-be reopen re-close without touching GitHub (#killswitch-gap)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentDryRun: true, autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); - await processJob(env, { type: "github-webhook", deliveryId: "reopen-dryrun", eventName: "pull_request", payload: reopenedPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toContain("dry-run: would re-close"); - }); - - it("allows an admin reopener to reopen without reclosing (fast-path hasMaintainerPermission)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push({ url, method: init?.method ?? "GET" }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory", ADMIN_GITHUB_LOGINS: "admin-user" }); - await processJob(env, { type: "github-webhook", deliveryId: "admin-reopen", eventName: "pull_request", payload: reopenedPayload("admin-user") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("allows reopen when the closer is unknown (null lastCloser)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push({ url, method: init?.method ?? "GET" }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.includes("/issues/42/events")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await processJob(env, { type: "github-webhook", deliveryId: "unknown-closer", eventName: "pull_request", payload: reopenedPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("re-closes when the close event is hidden beyond the inspected event window (window-evasion fail-closed, #audit-2.4)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.includes("/issues/42/events")) { - // Long timeline (lastPage=12): the contributor padded the events so the real close sits before the - // inspected newest window. No "closed" appears in the read pages → null closer + coveredAllPages=false. - // The tail DOES include the contributor's own "reopened" event (the one this whole handler is reacting - // to), so the new live re-check (#2369) still finds `contributor` as the current reopener and does not - // itself block the re-close — only the (deliberately fail-closed) window-evasion path above does. - const page = Number(new URL(url).searchParams.get("page") ?? "1"); - if (page === 1) { - return Response.json([{ event: "labeled", actor: { login: "contributor" } }], { - headers: { link: '; rel="last"' }, - }); - } - if (page === 12) return Response.json([{ event: "labeled", actor: { login: "contributor" } }, { event: "reopened", actor: { login: "contributor" } }]); - return Response.json([{ event: "labeled", actor: { login: "contributor" } }]); - } - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); // opted into acting autonomy - await processJob(env, { type: "github-webhook", deliveryId: "window-evasion-reclose", eventName: "pull_request", payload: reopenedPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - const audit = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ detail: string }>(); - expect(audit?.detail).toContain("beyond the inspected event window"); - }); - - it("re-closes when the bot itself was the last closer", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "gittensory[bot]" } }, { event: "reopened", actor: { login: "contributor" } }]); - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto", close: "auto" } }); // opted into acting autonomy - await processJob(env, { type: "github-webhook", deliveryId: "bot-closer-reclose", eventName: "pull_request", payload: reopenedPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - }); - - it("allows reopen when a contributor self-closed (non-maintainer, non-bot closer)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push({ url, method: init?.method ?? "GET" }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "contributor" } }]); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await processJob(env, { type: "github-webhook", deliveryId: "self-close-reopen", eventName: "pull_request", payload: reopenedPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("treats permission API errors as non-maintainer (catch path returns null)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push({ url, method: init?.method ?? "GET" }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/collaborators/") && url.endsWith("/permission")) throw new Error("permission API down"); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "contributor" } }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await processJob(env, { type: "github-webhook", deliveryId: "perm-api-error", eventName: "pull_request", payload: reopenedPayload("contributor") }); - // permission API threw → null → non-maintainer reopener + non-maintainer closer → no reclose. - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("swallows createIssueComment, closePullRequest, and recordAuditEvent errors on reclose (fail-safe — all .catch() bodies)", async () => { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - // createIssueComment (POST) and closePullRequest (PATCH) both throw → their .catch(() => undefined) bodies run - throw new Error("GitHub API unavailable"); - }); - vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await expect( - processJob(env, { type: "github-webhook", deliveryId: "reopen-api-fail-safe", eventName: "pull_request", payload: reopenedPayload("contributor") }), - ).resolves.toBeUndefined(); - }); - - it("REGRESSION (#4602): does NOT re-close a disallowed reopen when close autonomy is unconfigured, even though another class (merge) is auto", async () => { - // Before #4602, this guard gated only on isAgentConfigured(autonomy) -- true here because `merge` is - // acting -- with no check on the `close` action class specifically. A repo that opts into merge/review - // autonomy but deliberately leaves close unconfigured (deny-by-default) must NOT have PRs re-closed here. - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 99 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { merge: "auto", request_changes: "auto" } }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "reopen-close-autonomy-unconfigured", - eventName: "pull_request", - payload: reopenedPayload("contributor"), - }); - - expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(false); - expect(calls.some((call) => call.method === "POST" && call.url.endsWith("/issues/42/comments"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("autonomy for close is not acting"); - expect(audit?.detail).toContain("reopen re-close not enforced for contributor"); - }); - - it("REGRESSION (#4602): denies with an approval-required message when close autonomy is auto_with_approval", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.endsWith("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/collaborators/maintainer/permission")) return Response.json({ permission: "write" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "maintainer" } }, { event: "reopened", actor: { login: "contributor" } }]); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await repositoriesModule.upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", autonomy: { close: "auto_with_approval" } }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "reopen-close-autonomy-approval", - eventName: "pull_request", - payload: reopenedPayload("contributor"), - }); - - expect(calls.some((call) => call.method === "PATCH" && call.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("close autonomy requires approval"); - }); -}); - -describe("converted_to_draft gate-close (draft-dodge prevention)", () => { - beforeEach(() => clearInstallationTokenCacheForTest()); - afterEach(() => { - clearInstallationTokenCacheForTest(); - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - }); - - function draftPayload(author: string, headSha = "abc123", isDraft = true): any { - return { - action: "converted_to_draft", - installation: { id: 123 }, - repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, - sender: { login: author, type: "User" }, - pull_request: { - id: 4242, - number: 42, - state: "open", - title: "Some PR", - body: "Body.", - user: { login: author }, - head: { sha: headSha, ref: "fix", repo: { full_name: `${author}/gittensory`, owner: { login: author } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: isDraft, - merged: false, - mergeable_state: "clean", - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - }, - }; - } - - async function setupRepo(env: ReturnType, overrides: Record = {}): Promise { - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto" }, - agentPaused: false, - ...overrides, - }); - } - - it("closes a PR immediately when the contributor converts to draft after a gate failure on the same headSha", async () => { - const calls: Array<{ url: string; method: string; body?: unknown }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method, body: init?.body ? JSON.parse(String(init.body)) : undefined }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-1", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - const audit = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ detail: string }>(); - expect(audit?.detail).toContain("abc123"); - expect(audit?.detail).toContain("contributor"); - }); - - it("does NOT draft-dodge close when live PR state has moved since the webhook was received (#2130)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - // A maintainer merged/closed the PR — or a fresh commit resolved the gate failure — in the window between - // webhook ingestion and this handler's async DB reads (getGateBlockOutcome, isGlobalAgentFrozen). The live - // re-check must catch it and deny the close rather than firing blind off the stale ingestion-time payload. - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "closed", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "closed" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-stale", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("draft-dodge close not executed"); - }); - - it("REGRESSION: does NOT draft-dodge close when the PR was converted back to ready_for_review before the close fires (#2130 follow-up)", async () => { - // Same head, still open — a head/state-only freshness check would say "current". But the draft-dodge - // close's whole justification is "the author is dodging the gate via draft state", which no longer holds - // once the PR is ready_for_review again — closing here would be wrong even though nothing else moved. - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "no_longer_draft", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "open" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-no-longer-draft", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ requireDraft: true })); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("no longer a draft"); - }); - - it("swallows a recordAuditEvent failure on the stale-draft-dodge denial path — handler still completes (#2130)", async () => { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "closed", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "closed" }); - vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-stale-audit-fail", eventName: "pull_request", payload: draftPayload("contributor") }), - ).resolves.toBeUndefined(); - }); - - it("denies the draft-dodge close (never attempts it) when pull_requests: write is not granted (#2134)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - // Installation grant is missing pull_requests: write (revoked or never consented) — issues: write is present, - // so this isn't a blanket permission failure, just the specific scope this close needs. - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "enabled", reviewCheckMode: "required", autonomy: { close: "auto" }, agentPaused: false }); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-no-write", eventName: "pull_request", payload: draftPayload("contributor") }); - - // Neither the close nor its accompanying comment was attempted — a 403 from GitHub is never reached. - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("pull_requests: write not granted"); - }); - - it("denies the draft-dodge close when no installation row was pre-synced and the webhook payload carries no permissions", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - // No installations row pre-seeded. processGitHubWebhook auto-upserts one from the payload's bare - // `installation: { id: 123 }` (no permissions field, as a real pull_request payload carries), so the - // resulting row has no explicit pull_requests:write grant — the permission check must fail CLOSED (deny). - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "enabled", reviewCheckMode: "required", autonomy: { close: "auto" }, agentPaused: false }); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-no-install-row", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("denied"); - }); - - it("REGRESSION: a transient getInstallation read failure during the draft-dodge readiness check propagates (retries) instead of misrecording a permission denial", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "enabled", reviewCheckMode: "required", autonomy: { close: "auto" }, agentPaused: false }); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - // First getInstallation call in processGitHubWebhook (installationActor derivation, unrelated to this fix) - // resolves normally; the SECOND call is the draft-dodge readiness check itself -- that one is a genuine D1 - // read failure, not a "row not found." - const getInstallationSpy = vi.spyOn(repositoriesModule, "getInstallation"); - getInstallationSpy.mockResolvedValueOnce({ - id: 123, - accountLogin: "JSONbored", - accountId: 1, - appId: null, - targetType: "User", - repositorySelection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - suspendedAt: null, - createdAt: null, - updatedAt: null, - }); - getInstallationSpy.mockRejectedValueOnce(new Error("D1 read failed")); - - await expect(processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-install-read-fails", eventName: "pull_request", payload: draftPayload("contributor") })).rejects.toThrow("D1 read failed"); - - // Neither the close nor its accompanying comment was attempted -- the failure short-circuits before either. - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - // No misleading "pull_requests: write not granted" audit -- the webhook's own top-level catch records the - // actual error instead, which the queue's standard retry-on-throw semantics will re-attempt. - const draftDodgeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ n: number }>(); - expect(draftDodgeAudit?.n).toBe(0); - const webhookAudit = await env.DB.prepare("select status, error_summary from webhook_events where delivery_id = ?").bind("draft-dodge-install-read-fails").first<{ status: string; error_summary: string | null }>(); - expect(webhookAudit?.status).toBe("error"); - expect(webhookAudit?.error_summary).toContain("D1 read failed"); - }); - - it("does NOT draft-dodge close while the global freeze is on (#killswitch-gap)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - await repositoriesModule.setGlobalAgentFrozen(env, true); - await processJob(env, { type: "github-webhook", deliveryId: "draft-frozen", eventName: "pull_request", payload: draftPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed under freeze - expect(await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ n: number }>()).toMatchObject({ n: 0 }); - }); - - it("dry-run: audits a would-be draft-dodge close without touching GitHub (#killswitch-gap)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env, { agentDryRun: true }); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-dryrun", eventName: "pull_request", payload: draftPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // never closed in dry-run - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toContain("dry-run: would close"); - }); - - it("retries the draft-dodge close when a concurrent delivery already holds the per-PR actuation lock (#2447)", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - // Simulates a DIFFERENT concurrent delivery for the same PR already in flight (e.g. a check_suite completion - // racing this converted_to_draft event) — the lock key it would hold is pre-claimed here. - await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:jsonbored/gittensory#42", "1", 60); - - await expect(processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-lock-contended", eventName: "pull_request", payload: draftPayload("contributor") })).rejects.toThrow("pr actuation lock contended"); - - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ n: number }>(); - expect(audit?.n).toBe(0); // no decision recorded either way — the queue retry owns the deferred decision - }); - - it("REGRESSION: exactly ONE of two genuinely concurrent draft-dodge deliveries for the SAME PR wins the actuation lock (#2135)", async () => { - // Unlike the lock-contended test above (which pre-seeds the key before the call even starts), this fires - // two deliveries together via Promise.all with NEITHER pre-claiming anything — exercising the actual - // check-and-set race claimPrActuationLock must arbitrate, not just "the key was already there". A - // get-then-set (non-atomic) implementation lets both deliveries observe an absent key and both proceed, - // which this test would catch as more than one PATCH / more than one completed audit row. - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - const results = await Promise.allSettled([ - processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-race-a", eventName: "pull_request", payload: draftPayload("contributor") }), - processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-race-b", eventName: "pull_request", payload: draftPayload("contributor") }), - ]); - expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); - expect(results.filter((result) => result.status === "rejected")).toHaveLength(1); - - const patchCalls = calls.filter((c) => c.includes("PATCH") && c.includes("/pulls/42")); - expect(patchCalls).toHaveLength(1); // exactly one delivery won the race and closed the PR - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and outcome = 'completed'").bind("github_app.draft_dodge_closed").first<{ n: number }>(); - expect(audit?.n).toBe(1); // exactly one completed close recorded — not two (the race), not zero - }); - - it("no-ops when no prior gate failure exists for the PR", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - // No gate block recorded — gate hasn't run yet. - - await processJob(env, { type: "github-webhook", deliveryId: "draft-no-block", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - }); - - it("no-ops when the prior gate failure is for a different headSha (contributor pushed fixes in draft)", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - // Block exists but for an OLDER commit — contributor has pushed new code in draft. - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "old-sha-XYZ", blockerCodes: ["missing_linked_issue"] }); - - // Payload headSha is "abc123" (new commit), not "old-sha-XYZ". - await processJob(env, { type: "github-webhook", deliveryId: "draft-new-sha", eventName: "pull_request", payload: draftPayload("contributor", "abc123") }); - - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - }); - - it("no-ops when the gate block has been maintainer-overridden", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - await markGateOutcomeOverridden(env, "JSONbored/gittensory", 42); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-overridden", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - }); - - it("no-ops when the PR author is the repo owner (owner PRs are never auto-closed)", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - // Author = "JSONbored" = repo owner → no close. - await processJob(env, { type: "github-webhook", deliveryId: "draft-owner", eventName: "pull_request", payload: draftPayload("JSONbored") }); - - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - }); - - it("no-ops when the PR author is an ADMIN_GITHUB_LOGINS fleet-operator, not just the literal repo owner (#2133)", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory", ADMIN_GITHUB_LOGINS: "admin-user" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - // Author = "admin-user" ≠ repo owner "JSONbored", but IS in ADMIN_GITHUB_LOGINS → no close. - await processJob(env, { type: "github-webhook", deliveryId: "draft-admin", eventName: "pull_request", payload: draftPayload("admin-user") }); - - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - }); - - it("no-ops when the agent is paused (agentPaused=true)", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env, { agentPaused: true }); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-paused", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - }); - - it("no-ops when the agent autonomy is not configured (autonomy=null)", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push(`${init?.method ?? "GET"} ${url}`); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env, { autonomy: null }); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-no-autonomy", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - }); - - it("closes with empty blockerCodes (no codes parenthetical) and null author (uses 'unknown' in audit)", async () => { - // covers: codes ? `(${codes})` : "" → "" branch; pr.authorLogin ?? "unknown" → "unknown" branch - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push({ url, method: init?.method ?? "GET" }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42") && (init?.method ?? "GET") === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - // empty blockerCodes → codes = "" → ternary takes the "" branch - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: [] }); - - // null user.login → authorLogin null → (null ?? "").toLowerCase() === "" ≠ "jsonbored" → authorIsOwner false → close proceeds - // → pr.authorLogin ?? "unknown" in audit detail takes the "unknown" branch - const payload = draftPayload("contributor"); - payload.pull_request.user = { login: null }; - await processJob(env, { type: "github-webhook", deliveryId: "empty-codes-null-author", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - const comment = calls.find((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments")); - expect(comment).toBeDefined(); - const audit = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ detail: string }>(); - expect(audit?.detail).toContain("unknown"); - }); - - it("swallows createIssueComment and closePullRequest API errors (fail-safe — both .catch() bodies)", async () => { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - throw new Error("simulated network error"); // all GitHub calls throw - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - // Should not throw even though createIssueComment and closePullRequest both throw - await expect( - processJob(env, { type: "github-webhook", deliveryId: "api-error-swallow", eventName: "pull_request", payload: draftPayload("contributor") }), - ).resolves.toBeUndefined(); - - // Audit event was still written to DB (recordAuditEvent uses D1, not fetch) - const audit = await env.DB.prepare("select event_type from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ event_type: string }>(); - expect(audit?.event_type).toBe("github_app.draft_dodge_closed"); - }); - - it("getGateBlockOutcome DB error is caught — handler no-ops gracefully", async () => { - const calls: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - calls.push(`${init?.method ?? "GET"} ${input}`); - if (input.toString().includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - // Spy on getGateBlockOutcome to throw — the .catch(() => undefined) body must execute - vi.spyOn(repositoriesModule, "getGateBlockOutcome").mockRejectedValueOnce(new Error("D1 error")); - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "gbo-db-error", eventName: "pull_request", payload: draftPayload("contributor") }), - ).resolves.toBeUndefined(); - - // No close should have happened (block was unknown due to DB error) - expect(calls.some((c) => c.includes("PATCH") && c.includes("/pulls/42"))).toBe(false); - }); - - it("recordAuditEvent failure is swallowed — close still proceeds without crashing", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push({ url, method: init?.method ?? "GET" }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments") && (init?.method ?? "GET") === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42") && (init?.method ?? "GET") === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 write error")); - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "audit-db-error", eventName: "pull_request", payload: draftPayload("contributor") }), - ).resolves.toBeUndefined(); - - // Close still happened despite audit failure - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - }); - - it("no-op owner-exemption when repoFullName has no slash (repoOwner is empty — authorIsOwner always false)", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push({ url, method: init?.method ?? "GET" }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/issues/") && (init?.method ?? "GET") === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/pulls/") && (init?.method ?? "GET") === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - // Setup with a slash-free repo name - await upsertRepositoryFromGitHub(env, { name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }, 200); - await upsertInstallation(env, { - installation: { - id: 200, - account: { login: "", id: 2, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "noslash", full_name: "noslash", private: false, owner: { login: "" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "noslash", - gateCheckMode: "enabled", reviewCheckMode: "required", - autonomy: { close: "auto" }, - agentPaused: false, - }); - await recordGateBlockOutcome(env, { repoFullName: "noslash", pullNumber: 77, headSha: "sha-noslash", blockerCodes: ["missing_linked_issue"] }); - - const noslashPayload = { - action: "converted_to_draft", - installation: { id: 200 }, - repository: { id: 2, name: "noslash", full_name: "noslash", private: false, default_branch: "main", owner: { login: "" } }, - sender: { login: "someone", type: "User" }, - pull_request: { - id: 9999, - number: 77, - state: "open", - title: "slash-free", - body: "", - user: { login: "someone" }, - head: { sha: "sha-noslash", ref: "fix", repo: { full_name: "someone/noslash", owner: { login: "someone" } } }, - base: { sha: "base", ref: "main", repo: { full_name: "noslash", owner: { login: "" } } }, - draft: true, - merged: false, - mergeable_state: "clean", - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - }, - }; - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "noslash-test", eventName: "pull_request", payload: noslashPayload }), - ).resolves.toBeUndefined(); - - // With no slash in repoFullName: repoOwner="" (branch 196 false) → repoOwner.length>0=false (branch 198 false) - // → authorIsOwner=false → handler enters the close path. closePullRequest/.catch() swallows the splitRepo - // error (GitHub API requires owner/repo — slash-free names can't be closed via API) but the handler itself - // doesn't crash. Verify the handler DID reach getGateBlockOutcome, proving branches 196+198 were exercised. - const verifyBlock = await repositoriesModule.getGateBlockOutcome(env, "noslash", 77); - expect(verifyBlock?.headSha).toBe("sha-noslash"); - }); - - it("REGRESSION (#4602): does NOT draft-dodge close when close autonomy is unconfigured, even though another PR-write class (approve) is auto and pull_requests:write IS granted", async () => { - // Before #4602, resolveAgentPermissionReadiness's missing actionClass:"close" checked the UNION of every - // acting class's write-permission grant, not close's specifically -- `approve` is a PR-write class and - // pull_requests:write IS granted here (setupRepo's default), so readiness alone used to read "ready" and - // let the close proceed despite close itself never being authorized. - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.endsWith("/issues/42/comments") && method === "POST") return Response.json({ id: 1 }, { status: 201 }); - if (url.endsWith("/pulls/42") && method === "PATCH") return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env, { autonomy: { approve: "auto" } }); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-close-autonomy-unconfigured", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("autonomy for close is not acting"); - expect(audit?.detail).toContain("draft-dodge close not enforced for contributor"); - }); - - it("REGRESSION (#4602): denies with an approval-required message when close autonomy is auto_with_approval", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - calls.push({ url, method: init?.method ?? "GET" }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupRepo(env, { autonomy: { close: "auto_with_approval" } }); - await recordGateBlockOutcome(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", blockerCodes: ["missing_linked_issue"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-dodge-close-autonomy-approval", eventName: "pull_request", payload: draftPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("close autonomy requires approval"); - }); -}); - -function draftEvasionPayload(author: string, headSha = "abc123"): any { - return { - action: "converted_to_draft", - installation: { id: 123 }, - repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, - sender: { login: author, type: "User" }, - pull_request: { - id: 4242, - number: 42, - state: "open", - title: "Some PR", - body: "Body.", - user: { login: author }, - head: { sha: headSha, ref: "fix", repo: { full_name: `${author}/gittensory`, owner: { login: author } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: true, - merged: false, - mergeable_state: "clean", - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - }, - }; -} - -function closedPayload(sender: string, author = sender, headSha = "abc123"): any { - return { - action: "closed", - installation: { id: 123 }, - repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, - sender: { login: sender, type: "User" }, - pull_request: { - id: 4242, - number: 42, - state: "closed", - title: "Some PR", - body: "Body.", - user: { login: author }, - head: { sha: headSha, ref: "fix", repo: { full_name: `${author}/gittensory`, owner: { login: author } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: false, - merged: false, - mergeable_state: "clean", - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - }, - }; -} - -describe("review-evasion protection (#review-evasion-protection)", () => { - beforeEach(() => clearInstallationTokenCacheForTest()); - afterEach(() => { - clearInstallationTokenCacheForTest(); - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - }); - - async function setupEvasionRepo(env: ReturnType, overrides: Record = {}): Promise { - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - publicSurface: "off", - commentMode: "off", - checkRunMode: "off", - autonomy: { close: "auto" }, - agentPaused: false, - reviewEvasionProtection: "close", - ...overrides, - }); - } - - // Generic GitHub fetch stub covering every endpoint the evasion handlers (and the surrounding webhook - // pipeline they run inside) can call. `collaboratorPermission` controls what a non-owner/non-admin closer's - // permission check reports (default "read" — an ordinary contributor). - function stubEvasionFetch(calls: Array<{ url: string; method: string }>, opts: { collaboratorPermission?: string; onPatch?: (url: string) => Response | null } = {}) { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: opts.collaboratorPermission ?? "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) { - const custom = opts.onPatch?.(url); - if (custom) return custom; - return Response.json({ state: url === "open" ? "open" : "closed" }); - } - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/check-runs")) return Response.json({ id: 900 }, { status: 201 }); - if (url.includes("/labels")) return Response.json([{ name: "review-evasion" }]); - if (url.includes("/pulls/42/files")) return Response.json([]); - // A .gittensory.yml content fetch (raw.githubusercontent.com) must resolve to SOMETHING with no opinion - // on reviewEvasionProtection -- otherwise a miss here falls through to the bundled JSONbored/gittensory - // fallback manifest (gittensory-repo-focus-manifest.ts), whose OWN checked-in reviewEvasionProtection: - // close would silently outrank every test below's DB-level override (yml > DB precedence, #config-as-code). - if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); - return new Response("not found", { status: 404 }); - }); - } - - describe("self-close during an active review", () => { - it("reopens then re-closes as the App, posts the explanation comment, applies the label, and records a review_evasion strike", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", authorLogin: "contributor", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-1", eventName: "pull_request", payload: closedPayload("contributor") }); - - const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); - expect(patches.length).toBeGreaterThanOrEqual(2); // reopen (state=open) then re-close (state=closed) - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toContain("contributor"); - expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(false); // terminalized - const strike = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ outcome: string }>(); - expect(strike?.outcome).toBe("completed"); - }); - - it("reopens and re-closes when the live self-closed PR is already closed on the reviewed head", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", authorLogin: "contributor", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ - status: "stale", - reason: "closed", - expectedHeadSha: "abc123", - liveHeadSha: "ABC123", - liveState: "closed", - }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-live-closed", eventName: "pull_request", payload: closedPayload("contributor") }); - - const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); - expect(patches.length).toBeGreaterThanOrEqual(2); // same-head closed is the normal self-close state: reopen then re-close - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(false); - const strike = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ outcome: string }>(); - expect(strike?.outcome).toBe("completed"); - }); - - it("retries (via a thrown lock-contended error) when a concurrent delivery already holds the per-PR actuation lock", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:jsonbored/gittensory#42", "1", 60); - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "self-close-lock-contended", eventName: "pull_request", payload: closedPayload("contributor") }), - ).rejects.toThrow("during review-evasion-self-close"); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ n: number }>(); - expect(audit?.n).toBe(0); // no decision recorded either way -- the queue retry owns the deferred decision - }); - - it("does nothing when reviewEvasionProtection is explicitly off (#4011: the only respected opt-out)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { reviewEvasionProtection: "off" }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-off", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ n: number }>(); - expect(audit?.n).toBe(0); - }); - - it("does nothing when NO active review is tracked for this head (an ordinary close, nothing to evade)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - // No startActiveReviewTracking call at all. - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-active-review", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when a THIRD PARTY closed someone else's PR (not the author) — an ordinary maintainer close, not self-close evasion", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls, { collaboratorPermission: "write" }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-third-party", eventName: "pull_request", payload: closedPayload("a-maintainer", "contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the closer is the repo owner", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-owner", eventName: "pull_request", payload: closedPayload("JSONbored") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the closer is an ADMIN_GITHUB_LOGINS fleet-operator", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory", ADMIN_GITHUB_LOGINS: "admin-user" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-admin", eventName: "pull_request", payload: closedPayload("admin-user") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the closer holds write/maintain/admin collaborator permission", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls, { collaboratorPermission: "write" }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-maintainer", eventName: "pull_request", payload: closedPayload("write-collaborator") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing for a protected automation author (e.g. dependabot[bot])", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-bot", eventName: "pull_request", payload: closedPayload("dependabot[bot]") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("dry-run: audits the would-be enforcement without mutating GitHub or recording a live strike", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { agentDryRun: true }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-dry-run", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toContain("dry-run"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - }); - - it("denies enforcement when the agent is globally frozen", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await repositoriesModule.setGlobalAgentFrozen(env, true, "test"); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-frozen", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("paused"); - }); - - it("denies enforcement when close autonomy is not acting (observe)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { autonomy: { close: "observe" } }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-observe", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("autonomy for close is not acting"); - }); - - it("REGRESSION: denies live self-close enforcement when close autonomy requires approval", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-approval-required", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("requires approval"); - }); - - it("denies enforcement when pull_requests: write is not granted", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", publicSurface: "off", commentMode: "off", checkRunMode: "off", autonomy: { close: "auto" }, agentPaused: false, reviewEvasionProtection: "close" }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-write", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("pull_requests: write not granted"); - }); - - it("denies enforcement when the closed live PR is not on the reviewed head", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "closed", expectedHeadSha: "abc123", liveHeadSha: "def456", liveState: "closed" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-stale", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("review-evasion enforcement not executed"); - }); - - it("audits an error and does NOT record a strike when the reopen API call fails", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return new Response("server error", { status: 500 }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-reopen-fail", eventName: "pull_request", payload: closedPayload("contributor") }); - - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("error"); - expect(audit?.detail).toContain("FAILED to reopen"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - // The PR is closed either way (our reopen attempt failing doesn't reopen it) -- the general - // "closed"-action cleanup still terminalizes the tracking row, independent of enforcement success. - expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(false); - }); - - it("REGRESSION (gate-flagged): throws (never silently leaves the PR open) when reopen succeeds but the re-close API call fails, so the queue retries the job", async () => { - const calls: Array<{ url: string; method: string }> = []; - let patchCount = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) { - patchCount += 1; - if (patchCount === 1) return Response.json({ state: "open" }); // reopen succeeds - return new Response("server error", { status: 500 }); // re-close fails - } - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - // Deliberately UNCAUGHT: leaving the reopened PR open and returning normally would be worse than the - // contributor's original close, so this must propagate for the queue's own retry mechanism instead of - // resolving quietly. - await expect( - processJob(env, { type: "github-webhook", deliveryId: "self-close-close-fail", eventName: "pull_request", payload: closedPayload("contributor") }), - ).rejects.toThrow(); - - expect(patchCount).toBe(2); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("error"); - expect(audit?.detail).toContain("FAILED to re-close"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - // Still active -- enforcement never completed, so the active-review row must not have been cleared - // (the active-review-tracking cleanup below only fires on the "closed" webhook action's OWN pass, and - // this throw aborts that pass before it reaches the general terminalize hook). - expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(true); - }); - - it("REGRESSION (gate-flagged): a retry after the re-close failure converges -- the PR ends up closed, and the strike is recorded exactly once", async () => { - const calls: Array<{ url: string; method: string }> = []; - let closeAttempts = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) { - const body = init?.body ? JSON.parse(String(init.body)) : {}; - if (body.state === "open") return Response.json({ state: "open" }); // reopen always succeeds - closeAttempts += 1; - if (closeAttempts === 1) return new Response("server error", { status: 500 }); // FIRST close attempt fails - return Response.json({ state: "closed" }); // retry's close attempt succeeds - } - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/labels")) return Response.json([{ name: "review-evasion" }]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", authorLogin: "contributor", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - const payload = closedPayload("contributor"); - await expect(processJob(env, { type: "github-webhook", deliveryId: "self-close-close-fail-retry", eventName: "pull_request", payload })).rejects.toThrow(); - // The queue's own retry mechanism re-delivers the SAME job after the first attempt threw. - await processJob(env, { type: "github-webhook", deliveryId: "self-close-close-fail-retry", eventName: "pull_request", payload }); - - expect(closeAttempts).toBe(2); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? order by created_at desc limit 1").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - const strikeCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strikeCount?.n).toBe(1); // exactly one strike, not one per attempt - expect(await repositoriesModule.hasActiveReviewForHeadSha(env, "JSONbored/gittensory", 42, "abc123")).toBe(false); - }); - - it("global moderation disabled: the evasion close/label/comment still happen, but no moderation strike/label is recorded", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - // Global moderation config left at its default (disabled). - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-mod-off", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - }); - - it("REGRESSION: no duplicate strike or duplicate enforcement on a webhook redelivery/retry after the first enforcement already succeeded", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-redelivery-1", eventName: "pull_request", payload: closedPayload("contributor") }); - const firstPatchCount = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")).length; - expect(firstPatchCount).toBeGreaterThanOrEqual(2); - - // A SECOND, genuinely distinct delivery for the same underlying event (e.g. a queue retry after the first - // job's ack was lost) — the active-review row is already terminalized, so this must be a pure no-op. - await processJob(env, { type: "github-webhook", deliveryId: "self-close-redelivery-2", eventName: "pull_request", payload: closedPayload("contributor") }); - const secondPatchCount = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")).length - firstPatchCount; - expect(secondPatchCount).toBe(0); - - const strikeCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strikeCount?.n).toBe(1); - }); - - it("a subsequent contributor reopen after the App's evasion close is re-closed by the EXISTING one-shot reopen guard", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-then-reopen-1", eventName: "pull_request", payload: closedPayload("contributor") }); - expect((await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>())?.outcome).toBe("completed"); - - // getLastCloserLogin reads the issue-events timeline -- the App's own close (via the enforcement handler, - // NOT via the reopen-reclose guard) must be visible there for the existing guard to recognize it. - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/contributor/permission")) return Response.json({ permission: "read" }); - if (url.includes("/issues/42/events")) return Response.json([{ event: "closed", actor: { login: "gittensory[bot]" } }, { event: "reopened", actor: { login: "contributor" } }]); - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 2 }, { status: 201 }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - return new Response("not found", { status: 404 }); - }); - await processJob(env, { type: "github-webhook", deliveryId: "contributor-reopens-after-evasion-close", eventName: "pull_request", payload: reopenedPayload("contributor") }); - - const reopenAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.reopen_reclosed").first<{ outcome: string; detail: string }>(); - expect(reopenAudit?.outcome).toBe("completed"); - expect(reopenAudit?.detail).toContain("one-shot"); - }); - - it("STILL protects when reviewEvasionProtection is unset (undefined, not an explicit 'off') (#4011: default-ON)", async () => { - // upsertRepositorySettings coalesces undefined -> "close" at write time (mirrors reviewEvasionLabel/ - // reviewEvasionComment's own write-time defaulting below), and the consuming handler's own fallback - // (settings.reviewEvasionProtection === "off") treats anything but an explicit "off" as protected too -- - // so the only way to get `undefined` past BOTH layers and into the handler is to mock the resolved- - // settings layer directly, confirming neither layer silently reintroduces the old off-by-default gap. - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionProtection: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-protection-unset", eventName: "pull_request", payload: closedPayload("contributor") }); - - const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); - expect(patches.length).toBeGreaterThanOrEqual(2); // reopen then re-close, same as an explicit "close" - }); - - it("does nothing when the webhook payload has no sender", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - const payload = closedPayload("contributor"); - payload.sender = undefined; - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-sender", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the PR record has no author (a deleted-account PR)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - const payload = closedPayload("contributor"); - payload.pull_request.user = null; - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-author", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the PR record has no headSha", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - const payload = closedPayload("contributor"); - payload.pull_request.head = null; - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-head-sha", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("denies enforcement when the installation record is missing (uninstalled mid-flight)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - vi.spyOn(repositoriesModule, "getInstallation").mockResolvedValue(null); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-no-installation", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("pull_requests: write not granted"); - }); - - it("skips the courtesy comment when reviewEvasionComment is unset (defaults to true, but false is honored too)", async () => { - // Same write-time-coalescing note as the reviewEvasionProtection test above. - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionComment: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-comment-unset", eventName: "pull_request", payload: closedPayload("contributor") }); - - // reviewEvasionComment unset falls back to `true` -- the courtesy comment still posts. - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - }); - - it("applies no label when reviewEvasionLabel is explicitly null (a .gittensory.yml-only 'no label' override)", async () => { - const calls: Array<{ url: string; method: string }> = []; - const labelPostBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (method === "POST" && url.endsWith("/issues/42/labels")) { - labelPostBodies.push(String(init?.body ?? "")); - return Response.json([], { status: 200 }); - } - if (url.includes("/labels")) return Response.json([]); // dedup probe: no labels on the issue yet - if (url.includes("/pulls/42/files")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - // reviewEvasionLabel is a NOT NULL DB column (upsertRepositorySettings coalesces null -> the default at - // write time, per the migration's own "never persisted" comment) -- null only ever reaches this handler - // via the .gittensory.yml config-as-code layer, so the resolved-settings layer is mocked directly here. - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: null }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-label-null", eventName: "pull_request", payload: closedPayload("contributor") }); - - // Some OTHER unrelated feature (title-based type-labeling) may still post its own labels on a close -- - // what matters here is that the review-evasion label specifically was never requested. - expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(false); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - }); - - it("falls back to the default label when reviewEvasionLabel is unset", async () => { - const calls: Array<{ url: string; method: string }> = []; - const labelPostBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (method === "POST" && url.endsWith("/issues/42/labels")) { - labelPostBodies.push(String(init?.body ?? "")); - return Response.json([], { status: 200 }); - } - if (url.includes("/labels")) return Response.json([]); // dedup probe: no labels on the issue yet - if (url.includes("/pulls/42/files")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "self-close-label-unset", eventName: "pull_request", payload: closedPayload("contributor") }); - - expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(true); - }); - }); - - describe("converted_to_draft during an active review", () => { - it("closes as the App (no reopen needed), posts the explanation comment, applies the label, and records a review_evasion strike", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", authorLogin: "contributor", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); - expect(patches).toHaveLength(1); // no reopen needed -- a single close. - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toContain("draft-conversion"); - const strike = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ outcome: string }>(); - expect(strike?.outcome).toBe("completed"); - }); - - it("retries (via a thrown lock-contended error) when a concurrent delivery already holds the per-PR actuation lock", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - // Deliberately autonomy: {} (not {close: "auto"}) -- this repo's OUTER dispatch condition for the - // SIBLING draft-dodge guard requires isAgentConfigured(settings.autonomy), so with no acting autonomy - // class at all, draft-dodge's OWN lock-claim attempt is skipped entirely and this test genuinely - // exercises THIS handler's own lock claim/throw, not draft-dodge's (both guards fire on - // converted_to_draft and would otherwise race for the identical lock key). - await setupEvasionRepo(env, { autonomy: {} }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await env.SELFHOST_TRANSIENT_CACHE?.set("pr-actuation-lock:jsonbored/gittensory#42", "1", 60); - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-lock-contended", eventName: "pull_request", payload: draftEvasionPayload("contributor") }), - ).rejects.toThrow("during review-evasion-draft"); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ n: number }>(); - expect(audit?.n).toBe(0); - }); - - it("does nothing for a draft conversion BEFORE any active review has started", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - // No startActiveReviewTracking call -- no review has ever run for this PR. - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-active-review", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does NOT require a prior gate failure (unlike the draft-dodge guard) -- an active review alone is enough", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - // Deliberately NO recordGateBlockOutcome call -- the draft-dodge guard's own trigger condition is absent. - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-gate-failure", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - const draftDodgeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.draft_dodge_closed").first<{ n: number }>(); - expect(draftDodgeAudit?.n).toBe(0); // the SIBLING guard never fired -- this is genuinely the new path. - const evasionAudit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); - expect(evasionAudit?.outcome).toBe("completed"); - }); - - it("does nothing when the author holds write collaborator permission", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls, { collaboratorPermission: "write" }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-maintainer", eventName: "pull_request", payload: draftEvasionPayload("write-collaborator") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("REGRESSION (gate-flagged): does nothing when a THIRD PARTY converts someone else's PR to draft (not the author) -- an ordinary maintainer action, not self-evasion, must never be enforced against the author who didn't do it", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls, { collaboratorPermission: "write" }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - const payload = draftEvasionPayload("contributor"); - payload.sender = { login: "a-maintainer", type: "User" }; // the CONVERTER, distinct from pull_request.user (the author) - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-third-party", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ n: number }>(); - expect(audit?.n).toBe(0); - }); - - it("dry-run: audits the would-be enforcement without mutating GitHub or recording a live strike", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { agentDryRun: true }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-dry-run", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toContain("dry-run"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - }); - - it("denies enforcement when the agent is globally frozen", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await repositoriesModule.setGlobalAgentFrozen(env, true, "test"); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-frozen", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("paused"); - }); - - it("denies enforcement when close autonomy is not acting (observe)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { autonomy: { close: "observe" } }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-observe", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("autonomy for close is not acting"); - }); - - it("REGRESSION: denies live draft-conversion enforcement when close autonomy requires approval", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-approval-required", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("requires approval"); - }); - - it("denies enforcement when pull_requests: write is not granted", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", publicSurface: "off", commentMode: "off", checkRunMode: "off", autonomy: { close: "auto" }, agentPaused: false, reviewEvasionProtection: "close" }); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-write", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("pull_requests: write not granted"); - }); - - it("denies enforcement when the PR was converted back to ready_for_review before the close fires (requireDraft freshness)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "no_longer_draft", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "open" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-longer-draft", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ requireDraft: true })); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - }); - - it("audits an error and does NOT record a strike when the close API call fails", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return new Response("server error", { status: 500 }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-close-fail", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("error"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - }); - - it("global moderation disabled: the evasion close/label/comment still happen, but no moderation strike is recorded", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-mod-off", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - }); - - it("STILL protects when reviewEvasionProtection is unset (undefined, not an explicit 'off') (#4011: default-ON)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionProtection: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-protection-unset", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - }); - - it("does nothing when the webhook payload has no sender", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - const payload = draftEvasionPayload("contributor"); - payload.sender = undefined; - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-sender", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the PR record has no author (a deleted-account PR)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - const payload = draftEvasionPayload("contributor"); - payload.pull_request.user = null; - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-author", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing for a protected automation author (e.g. dependabot[bot])", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-bot", eventName: "pull_request", payload: draftEvasionPayload("dependabot[bot]") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the PR record has no headSha", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - - const payload = draftEvasionPayload("contributor"); - payload.pull_request.head = null; - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-head-sha", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("denies enforcement when the installation record is missing (uninstalled mid-flight)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - vi.spyOn(repositoriesModule, "getInstallation").mockResolvedValue(null); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-no-installation", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("pull_requests: write not granted"); - }); - - it("skips the courtesy comment when reviewEvasionComment is unset (defaults to true, but false is honored too)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionComment: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-comment-unset", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - }); - - it("applies no label when reviewEvasionLabel is explicitly null (a .gittensory.yml-only 'no label' override)", async () => { - const calls: Array<{ url: string; method: string }> = []; - const labelPostBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (method === "POST" && url.endsWith("/issues/42/labels")) { - labelPostBodies.push(String(init?.body ?? "")); - return Response.json([], { status: 200 }); - } - if (url.includes("/labels")) return Response.json([]); // dedup probe: no labels on the issue yet - if (url.includes("/pulls/42/files")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: null }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-label-null", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(false); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - }); - - it("falls back to the default label when reviewEvasionLabel is unset", async () => { - const calls: Array<{ url: string; method: string }> = []; - const labelPostBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (method === "POST" && url.endsWith("/issues/42/labels")) { - labelPostBodies.push(String(init?.body ?? "")); - return Response.json([], { status: 200 }); - } - if (url.includes("/labels")) return Response.json([]); // dedup probe: no labels on the issue yet - if (url.includes("/pulls/42/files")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-label-unset", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(true); - }); - }); - - describe("repeated ready<->draft cycling (#gaming-tactic-draft-cycle)", () => { - it("does nothing on the FIRST draft conversion, then closes on the SECOND -- independent of active-review/gate-block state", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - // Deliberately NO startActiveReviewTracking / recordGateBlockOutcome call -- neither sibling guard's own - // trigger condition is present, so any close observed below can only be this new, count-based guard. - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); - expect(patches).toHaveLength(1); - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toContain("repeated draft-cycling"); - expect(audit?.detail).toContain("#2"); - const strike = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ outcome: string }>(); - expect(strike?.outcome).toBe("completed"); - }); - - it("does nothing when reviewEvasionProtection is off, even after a repeated cycle", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { reviewEvasionProtection: "off" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-off-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-off-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("STILL enforces the repeated-cycle close when reviewEvasionProtection is unset (undefined, not an explicit 'off') (#4011: default-ON)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionProtection: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-unset-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); // first conversion never closes - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-unset-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - const patches = calls.filter((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42")); - expect(patches).toHaveLength(1); // second conversion closes, same as an explicit "close" - }); - - it("REGRESSION (gate-flagged): does not enforce against a THIRD PARTY repeatedly converting someone else's PR to draft", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls, { collaboratorPermission: "write" }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - const payload = draftEvasionPayload("contributor"); - payload.sender = { login: "a-maintainer", type: "User" }; - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-party-1", eventName: "pull_request", payload }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-party-2", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("REGRESSION (gate-flagged, gittensory-orb review): a maintainer's draft conversion must NOT count toward the author's own cycle -- the author's first-ever conversion is never enforced even after a prior third-party one", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls, { collaboratorPermission: "write" }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - const maintainerConversion = draftEvasionPayload("contributor"); - maintainerConversion.sender = { login: "a-maintainer", type: "User" }; - - // A maintainer converts the contributor's PR to draft first. - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-mixed-1", eventName: "pull_request", payload: maintainerConversion }); - // Without the fix, this maintainer action would have already bumped the shared counter to 1. - const afterMaintainer = await env.DB.prepare("select draft_conversion_count as n from pull_requests where repo_full_name = ? and number = 42") - .bind("JSONbored/gittensory") - .first<{ n: number }>(); - expect(afterMaintainer?.n).toBe(0); // the maintainer's own conversion never counted at all. - - // The AUTHOR now converts their OWN PR to draft for the very first time -- ordinary WIP behavior. - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-mixed-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const afterAuthor = await env.DB.prepare("select draft_conversion_count as n from pull_requests where repo_full_name = ? and number = 42") - .bind("JSONbored/gittensory") - .first<{ n: number }>(); - expect(afterAuthor?.n).toBe(1); // the author's first conversion is counted as their first, not their second. - }); - - it("does nothing for a protected automation author (e.g. dependabot[bot]), even after a repeated cycle", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-bot-1", eventName: "pull_request", payload: draftEvasionPayload("dependabot[bot]") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-bot-2", eventName: "pull_request", payload: draftEvasionPayload("dependabot[bot]") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the PR record has no headSha, even after a repeated cycle", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - const payload = draftEvasionPayload("contributor"); - payload.pull_request.head = null; - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-head-1", eventName: "pull_request", payload }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-head-2", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the author holds write collaborator permission, even after a repeated cycle", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls, { collaboratorPermission: "write" }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-maintainer-1", eventName: "pull_request", payload: draftEvasionPayload("write-collaborator") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-maintainer-2", eventName: "pull_request", payload: draftEvasionPayload("write-collaborator") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("denies enforcement when close autonomy is not acting (observe)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { autonomy: { close: "observe" } }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-observe-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-observe-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("autonomy for close is not acting"); - }); - - it("REGRESSION: denies live repeated draft-cycling enforcement when close autonomy requires approval", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-approval-required-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-approval-required-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("requires approval"); - }); - - it("dry-run: audits the would-be enforcement without mutating GitHub or recording a live strike", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { agentDryRun: true }); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-dry-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-dry-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - expect(audit?.detail).toContain("dry-run"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - }); - - it("denies enforcement when the agent is paused for this repo", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { agentPaused: true }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-paused-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-paused-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("paused"); - }); - - it("denies enforcement when pull_requests: write is not granted", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertInstallation(env, { - installation: { - id: 123, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", publicSurface: "off", commentMode: "off", checkRunMode: "off", autonomy: { close: "auto" }, agentPaused: false, reviewEvasionProtection: "close" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-write-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-write-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("pull_requests: write not granted"); - }); - - it("denies enforcement when the PR was converted back to ready_for_review before the close fires (requireDraft freshness)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-fresh-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - vi.mocked(fetchPullRequestFreshness).mockResolvedValueOnce({ status: "stale", reason: "no_longer_draft", expectedHeadSha: "abc123", liveHeadSha: "abc123", liveState: "open" }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-fresh-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - expect(fetchPullRequestFreshness).toHaveBeenCalledWith(env, expect.objectContaining({ requireDraft: true })); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - }); - - it("audits an error and does NOT record a strike when the close API call fails", async () => { - const calls: Array<{ url: string; method: string }> = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return new Response("server error", { status: 500 }); - if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await repositoriesModule.upsertGlobalModerationConfig(env, { enabled: true, rules: ["review_evasion"] }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-close-fail-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-close-fail-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("error"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - }); - - it("global moderation disabled: the close/label/comment still happen, but no moderation strike is recorded", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-mod-off-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-mod-off-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("completed"); - const strike = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("moderation.violation.review_evasion").first<{ n: number }>(); - expect(strike?.n).toBe(0); - }); - - it("REGRESSION: the third (and every later) conversion is enforced too, not just exactly the second", async () => { - const calls: Array<{ url: string; method: string }> = []; - let patchCount = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) { - patchCount += 1; - return Response.json({ state: "open" }); // simulate the close failing to stick / a reopen between cycles - } - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (url.includes("/labels")) return Response.json([{ name: "review-evasion" }]); - if (url.includes("/pulls/42/files")) return Response.json([]); - if (url.includes("raw.githubusercontent.com") && url.includes("gittensory.y")) return new Response("source: repo_file\n", { status: 200 }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-third-3", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(patchCount).toBe(2); // enforced on the 2nd AND the 3rd -- >= 2, not === 2. - const completed = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and outcome = 'completed'") - .bind("github_app.review_evasion_closed") - .first<{ n: number }>(); - expect(completed?.n).toBe(2); - }); - - it("REGRESSION: the first conversion returns before the repeated-cycle lock so a retry cannot double-count it", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - // Deliberately autonomy: {} -- draft-dodge's own outer dispatch condition (isAgentConfigured) is false, so - // ITS lock claim never fires. The remaining sibling (review-evasion-active-review) has no settings gate at - // its OWN lock claim, so it claims+releases the lock normally on every converted_to_draft delivery. THIS - // guard now checks reviewEvasionProtection/count BEFORE claiming its own lock (#nit-lock-contention), so it - // never attempts a claim at all until draftConversionCount reaches 2 -- the first delivery below produces - // only the sibling's claim (mocked to succeed); the second produces the sibling's claim (succeeds) THEN - // this guard's own first-ever claim attempt, which is the one mocked to fail here. - await setupEvasionRepo(env, { autonomy: {} }); - const claimSpy = vi.spyOn(env.SELFHOST_TRANSIENT_CACHE!, "claim").mockResolvedValueOnce(true).mockResolvedValueOnce(true).mockResolvedValueOnce(false); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-lock-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - expect(claimSpy).toHaveBeenCalledTimes(1); // count is only 1 -- this guard never attempted a claim yet. - - await expect( - processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-lock-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }), - ).rejects.toThrow("during review-evasion-draft-cycle"); - - expect(claimSpy).toHaveBeenCalledTimes(3); - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the webhook payload has no sender, even after a repeated cycle", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - const payload = draftEvasionPayload("contributor"); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-sender-1", eventName: "pull_request", payload: { ...payload, sender: undefined } }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-sender-2", eventName: "pull_request", payload: { ...payload, sender: undefined } }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("does nothing when the PR record has no author (a deleted-account PR), even after a repeated cycle", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - const payload = draftEvasionPayload("contributor"); - payload.pull_request.user = null; - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-author-1", eventName: "pull_request", payload }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-author-2", eventName: "pull_request", payload }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - }); - - it("denies enforcement when the installation record is missing (uninstalled mid-flight)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-install-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - vi.spyOn(repositoriesModule, "getInstallation").mockResolvedValue(null); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-no-install-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false); - const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1") - .bind("github_app.review_evasion_closed") - .first<{ outcome: string; detail: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toContain("pull_requests: write not granted"); - }); - - it("skips the courtesy comment when reviewEvasionComment is explicitly false", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env, { reviewEvasionComment: false }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-comment-false-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-comment-false-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(true); - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(false); - }); - - it("posts the courtesy comment when reviewEvasionComment is unset (undefined, not just a stored default)", async () => { - const calls: Array<{ url: string; method: string }> = []; - stubEvasionFetch(calls); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-comment-unset-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionComment: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-comment-unset-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(calls.some((c) => c.method === "POST" && c.url.endsWith("/issues/42/comments"))).toBe(true); - }); - - it("applies no label when reviewEvasionLabel is explicitly null (a .gittensory.yml-only 'no label' override)", async () => { - const calls: Array<{ url: string; method: string }> = []; - const labelPostBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (method === "POST" && url.endsWith("/issues/42/labels")) { - labelPostBodies.push(String(init?.body ?? "")); - return Response.json([], { status: 200 }); - } - if (url.includes("/labels")) return Response.json([]); - if (url.includes("/pulls/42/files")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-label-null-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: null }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-label-null-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(false); - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ? order by rowid desc limit 1").bind("github_app.review_evasion_closed").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); - }); - - it("falls back to the default label when reviewEvasionLabel is unset (undefined, not just a stored default)", async () => { - const calls: Array<{ url: string; method: string }> = []; - const labelPostBodies: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - calls.push({ url, method }); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - if (url.includes("/collaborators/")) return Response.json({ permission: "read" }); - if (method === "PATCH" && url.endsWith("/pulls/42")) return Response.json({ state: "closed" }); - if (method === "POST" && url.endsWith("/issues/42/comments")) return Response.json({ id: 1 }, { status: 201 }); - if (method === "POST" && url.endsWith("/issues/42/labels")) { - labelPostBodies.push(String(init?.body ?? "")); - return Response.json([], { status: 200 }); - } - if (url.includes("/labels")) return Response.json([]); - if (url.includes("/pulls/42/files")) return Response.json([]); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - await setupEvasionRepo(env); - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-label-unset-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - const baseSettings = await repositorySettingsModule.resolveRepositorySettings(env, "JSONbored/gittensory"); - vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockResolvedValue({ ...baseSettings, reviewEvasionLabel: undefined }); - - await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-label-unset-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") }); - - expect(labelPostBodies.some((b) => b.includes("review-evasion"))).toBe(true); - }); - }); - - describe("bumpPullRequestDraftConversionCount", () => { - it("increments across repeated calls for the same PR and is independent of head SHA", async () => { - const env = createTestEnv({}); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - id: 4242, - number: 77, - state: "open", - title: "Some PR", - user: { login: "contributor" }, - head: { sha: "sha-1", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: false, - merged: false, - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - } as never); - - expect(await repositoriesModule.bumpPullRequestDraftConversionCount(env, "JSONbored/gittensory", 77)).toBe(1); - expect(await repositoriesModule.bumpPullRequestDraftConversionCount(env, "JSONbored/gittensory", 77)).toBe(2); - // A fresh push (new head SHA) between cycles must NOT reset the counter -- unlike mergeAttemptCount. - await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - id: 4242, - number: 77, - state: "open", - title: "Some PR", - user: { login: "contributor" }, - head: { sha: "sha-2", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: false, - merged: false, - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - } as never); - expect(await repositoriesModule.bumpPullRequestDraftConversionCount(env, "JSONbored/gittensory", 77)).toBe(3); - }); - - it("returns 0 for a PR that does not exist (no row to increment)", async () => { - const env = createTestEnv({}); - expect(await repositoriesModule.bumpPullRequestDraftConversionCount(env, "JSONbored/gittensory", 999999)).toBe(0); - }); - }); -}); - -describe("markPullRequestLinkedIssueHardRuleViolated (#linked-issue-hard-rule-persistence)", () => { - it("sets violatedAt + the reason on the first call and never overwrites them on a later call", async () => { - const env = createTestEnv({}); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - id: 5151, - number: 88, - state: "open", - title: "Some PR", - user: { login: "contributor" }, - head: { sha: "sha-1", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: false, - merged: false, - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - } as never); - - const before = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 88); - expect(before?.linkedIssueHardRuleViolatedAt).toBeNull(); - expect(before?.linkedIssueHardRuleViolationReason).toBeNull(); - - await repositoriesModule.markPullRequestLinkedIssueHardRuleViolated(env, "JSONbored/gittensory", 88, "Linked issue #7 is assigned to the maintainer (@JSONbored)"); - const afterFirst = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 88); - expect(afterFirst?.linkedIssueHardRuleViolatedAt).toEqual(expect.any(String)); - expect(afterFirst?.linkedIssueHardRuleViolationReason).toBe("Linked issue #7 is assigned to the maintainer (@JSONbored)"); - - // A SECOND confirmed violation (e.g. against a different linked issue, or a re-detected same one) must not - // move the timestamp or replace the reason -- the FIRST confirmed violation is what's remembered forever. - await repositoriesModule.markPullRequestLinkedIssueHardRuleViolated(env, "JSONbored/gittensory", 88, "Linked issue #9 is already assigned to @someone-else"); - const afterSecond = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 88); - expect(afterSecond?.linkedIssueHardRuleViolatedAt).toBe(afterFirst?.linkedIssueHardRuleViolatedAt); - expect(afterSecond?.linkedIssueHardRuleViolationReason).toBe("Linked issue #7 is assigned to the maintainer (@JSONbored)"); - - // A fresh push (new head SHA) between violations must NOT reset either field -- unlike mergeBlockedSha, - // this marker is deliberately not scoped to head SHA. - await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - id: 5151, - number: 88, - state: "open", - title: "Some PR", - user: { login: "contributor" }, - head: { sha: "sha-2", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: false, - merged: false, - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - } as never); - const afterNewHead = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 88); - expect(afterNewHead?.linkedIssueHardRuleViolatedAt).toBe(afterFirst?.linkedIssueHardRuleViolatedAt); - expect(afterNewHead?.linkedIssueHardRuleViolationReason).toBe("Linked issue #7 is assigned to the maintainer (@JSONbored)"); - }); - - it("truncates an overlong reason to 280 chars, mirroring markPullRequestMergeBlocked", async () => { - const env = createTestEnv({}); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await repositoriesModule.upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - id: 5152, - number: 89, - state: "open", - title: "Some PR", - user: { login: "contributor" }, - head: { sha: "sha-1", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: false, - merged: false, - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - } as never); - - const longReason = "x".repeat(400); - await repositoriesModule.markPullRequestLinkedIssueHardRuleViolated(env, "JSONbored/gittensory", 89, longReason); - const row = await repositoriesModule.getPullRequest(env, "JSONbored/gittensory", 89); - expect(row?.linkedIssueHardRuleViolationReason).toHaveLength(280); - }); - - it("is a safe no-op when the PR row does not exist yet", async () => { - const env = createTestEnv({}); - await expect(repositoriesModule.markPullRequestLinkedIssueHardRuleViolated(env, "JSONbored/gittensory", 999999, "unreachable")).resolves.toBeUndefined(); - }); -}); - -describe("recordAgentCommandUsage (signal-snapshot fail-safe)", () => { - afterEach(() => { - vi.restoreAllMocks(); - vi.unstubAllGlobals(); - }); - - it("swallows persistSignalSnapshot errors — catch body runs without crashing the handler", async () => { - // Bot-authored @gittensory comment hits the early bot_author bail-out path in - // maybeProcessGittensoryMentionCommand, which calls recordAgentCommandUsage. Injecting a - // persistSignalSnapshot failure exercises the catch at the bottom of that function. - vi.spyOn(repositoriesModule, "persistSignalSnapshot").mockRejectedValueOnce(new Error("signal DB error")); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - if (input.toString().includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - const payload: any = { - action: "created", - installation: { id: 123 }, - repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, - sender: { login: "gittensory[bot]", type: "Bot" }, - comment: { id: 999, body: "@gittensory help", user: { login: "gittensory[bot]", type: "Bot" } }, - issue: { id: 1, number: 77, title: "some issue", pull_request: { url: "https://api.github.com/repos/JSONbored/gittensory/pulls/77" } }, - }; - await expect( - processJob(env, { type: "github-webhook", deliveryId: "bot-mention-signal-fail", eventName: "issue_comment", payload }), - ).resolves.toBeUndefined(); - }); - - it("ignores a @gittensory mention on an EDITED comment — only newly-created comments are answered (#review-audit)", async () => { - const posts: string[] = []; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - if ((init?.method ?? "GET") === "POST" && url.includes("/comments")) posts.push(url); - if (url.includes("/access_tokens")) return Response.json({ token: "t" }); - return new Response("not found", { status: 404 }); - }); - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" }); - const payload: any = { - action: "edited", // an edit re-fires issue_comment with a NEW delivery id — the handler must NOT re-answer - installation: { id: 123 }, - repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, - sender: { login: "maintainer", type: "User" }, - comment: { id: 999, body: "@gittensory ask is this mergeable?", user: { login: "maintainer", type: "User" } }, - issue: { id: 1, number: 77, title: "some issue", pull_request: { url: "https://api.github.com/repos/JSONbored/gittensory/pulls/77" } }, - }; - await processJob(env, { type: "github-webhook", deliveryId: "mention-edited", eventName: "issue_comment", payload }); - expect(posts).toEqual([]); // the action guard returns false → no answer card posted - }); -}); - -function generateRsaPrivateKeyPem(): string { - const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); - return privateKey.export({ type: "pkcs1", format: "pem" }).toString(); -} - -function reopenedPayload(sender: string): any { - return { - action: "reopened", - installation: { id: 123 }, - repository: { id: 1, name: "gittensory", full_name: "JSONbored/gittensory", private: false, default_branch: "main", owner: { login: "JSONbored" } }, - sender: { login: sender, type: "User" }, - pull_request: { - id: 4242, - number: 42, - state: "open", - title: "Fix queued guard", - body: "Fixes the queued guard.", - user: { login: "contributor" }, - head: { sha: "abc123", ref: "fix", repo: { full_name: "contributor/gittensory", owner: { login: "contributor" } } }, - base: { sha: "base123", ref: "main", repo: { full_name: "JSONbored/gittensory", owner: { login: "JSONbored" } } }, - draft: false, - merged: false, - mergeable_state: "clean", - created_at: "2026-05-27T00:00:00Z", - updated_at: "2026-05-27T00:00:00Z", - }, - }; -} - -describe("installation app_id capture + dual-app webhook filter (#selfhost-app-id)", () => { - it("captures app_id from an installation payload, returns it, and preserves it when a later payload omits it", async () => { - const env = createTestEnv(); - const stored = await upsertInstallation(env, { - action: "created", - installation: { id: 4242, app_id: 555, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] }, - }); - expect(stored).toBe(555); - expect((await getInstallation(env, 4242))?.appId).toBe(555); - // A subsequent payload WITHOUT app_id (e.g. a pull_request event) must not clear the stored value. - const preserved = await upsertInstallation(env, { action: "synchronize", installation: { id: 4242, account: { login: "owner", id: 1, type: "Organization" } } }); - expect(preserved).toBe(555); - expect((await getInstallation(env, 4242))?.appId).toBe(555); - }); - - it("acks a webhook whose installation belongs to a DIFFERENT app without processing it", async () => { - const env = createTestEnv(); // own GITHUB_APP_ID defaults to "3824093" - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 7777); - // The installation is recorded as belonging to a FOREIGN app (99999 ≠ 3824093). - await upsertInstallation(env, { action: "created", installation: { id: 7777, app_id: 99999, account: { login: "JSONbored", id: 1, type: "User" }, repository_selection: "selected", permissions: {}, events: [] } }); - vi.stubGlobal("fetch", async () => Response.json({})); - - await processJob(env, { - type: "github-webhook", - deliveryId: "foreign-app-pr", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 7777 }, // a PR event carries no app_id; the stored 99999 is used - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 88, title: "Foreign", state: "open", user: { login: "contributor" }, head: { sha: "f88" }, labels: [], body: "x" }, - }, - }); - - // The delivery was acked as foreign, and the PR was never upserted (the handler returned before the PR block). - const evt = await env.DB.prepare("select payload_hash from webhook_events where delivery_id = ?").bind("foreign-app-pr").first<{ payload_hash: string }>(); - expect(evt?.payload_hash).toBe("foreign_app"); - const pr = await env.DB.prepare("select count(*) as n from pull_requests where repo_full_name = ? and number = ?").bind("JSONbored/gittensory", 88).first<{ n: number }>(); - expect(pr?.n).toBe(0); - }); - - it("processes a webhook whose installation app_id matches this backend (no false filtering)", async () => { - const env = createTestEnv(); // own GITHUB_APP_ID "3824093" - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 3824093001); - await upsertInstallation(env, { action: "created", installation: { id: 3824093001, app_id: 3824093, account: { login: "JSONbored", id: 1, type: "User" }, repository_selection: "selected", permissions: {}, events: [] } }); - vi.stubGlobal("fetch", async () => Response.json({})); - - await processJob(env, { - type: "github-webhook", - deliveryId: "own-app-pr", - eventName: "pull_request", - payload: { - action: "opened", - installation: { id: 3824093001 }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 89, title: "Own", state: "open", user: { login: "contributor" }, head: { sha: "o89" }, labels: [], body: "x" }, - }, - }); - - // The matching-app webhook was processed normally — the PR row exists and it was NOT acked as foreign. - const pr = await env.DB.prepare("select count(*) as n from pull_requests where repo_full_name = ? and number = ?").bind("JSONbored/gittensory", 89).first<{ n: number }>(); - expect(pr?.n).toBe(1); - const evt = await env.DB.prepare("select payload_hash from webhook_events where delivery_id = ?").bind("own-app-pr").first<{ payload_hash: string }>(); - expect(evt?.payload_hash).not.toBe("foreign_app"); - }); - - // #2537: durable PR-state cache — webhook invalidation + the act-boundary regression. - describe("durable PR-state cache (#2537)", () => { - function seedWarmPrStateCache(env: Env, repoFullName: string, pullNumber: number): Promise { - return upsertPullRequestDetailSyncState(env, { - repoFullName, - pullNumber, - status: "complete", - prMergeableState: "clean", - prState: "open", - prStateFetchedAt: new Date().toISOString(), - }); - } - - it.each(["synchronize", "closed", "reopened"] as const)( - "pull_request %s action invalidates the durable PR-state cache", - async (action) => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await seedWarmPrStateCache(env, "JSONbored/gittensory", 200); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "tok" }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: `invalidate-pr-state-${action}`, - eventName: "pull_request", - payload: { - action, - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 200, title: "PR", state: action === "closed" ? "closed" : "open", user: { login: "contributor" }, head: { sha: "a200" }, labels: [], body: "" }, - }, - }); - - expect(await getPullRequestDetailSyncState(env, "JSONbored/gittensory", 200)).toMatchObject({ - prMergeableState: null, - prState: null, - prStateFetchedAt: null, - }); - }, - ); - - it("a non-invalidating pull_request action (labeled) leaves the durable PR-state cache UNCHANGED", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { action: "created", installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" }, target_type: "User", repository_selection: "all", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await seedWarmPrStateCache(env, "JSONbored/gittensory", 201); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "tok" }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "invalidate-pr-state-labeled", - eventName: "pull_request", - payload: { - action: "labeled", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, - pull_request: { number: 201, title: "PR", state: "open", user: { login: "contributor" }, head: { sha: "a201" }, labels: [], body: "" }, - }, - }); - - expect(await getPullRequestDetailSyncState(env, "JSONbored/gittensory", 201)).toMatchObject({ - prMergeableState: "clean", - prState: "open", - }); - }); - - it("REGRESSION (#2537, gate-flagged): reconcileLiveDuplicateSiblings must NOT serve a warm durable PR-state cache row — a cached 'open' read up to PR_STATE_CACHE_MAX_AGE_MS stale after a missed closed webhook would keep an already-closed sibling eligible as the duplicate-cluster winner, wrongly closing the CURRENT PR as the loser", async () => { - const env = createTestEnv({ GITTENSORY_DUPLICATE_WINNER: "true" }); - // Seed a WARM cache row claiming the sibling is still open, but the live GitHub state below says CLOSED — - // proving the cache is never consulted: only a genuine live read can discover this and correctly reconcile it. - await seedWarmPrStateCache(env, "owner/repo", 5); - let liveStateFetches = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "tok" }); - if (/\/pulls\/5(?:\?|$)/.test(url)) { - liveStateFetches += 1; - return Response.json({ number: 5, state: "closed" }); - } - return Response.json({}); - }); - - const winner: Parameters[3] = { repoFullName: "owner/repo", number: 10, title: "Winner", state: "open", labels: [], linkedIssues: [1] }; - const sibling: Parameters[3] = { repoFullName: "owner/repo", number: 5, title: "Sibling", state: "open", labels: [], linkedIssues: [1] }; - const result = await reconcileLiveDuplicateSiblings(env, null, "owner/repo", winner, [sibling]); - - // The sibling is correctly dropped as stale-closed, proving a genuine live fetch happened rather than - // trusting the warm-but-wrong cached "open" value. - expect(result).toEqual([]); - expect(liveStateFetches).toBe(1); - }); - - it("REGRESSION (#2537): the per-PR sweep unit's live resync primes the durable PR-state cache for later readers", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { action: "created", installation: { id: 9001, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: { pull_requests: "write" }, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9001); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", gateCheckMode: "off", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 6, title: "Sweep target", state: "open", user: { login: "contributor" }, head: { sha: "a6" }, base: { ref: "main" }, labels: [], body: "" }); - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes("/access_tokens")) return Response.json({ token: "tok" }); - if (/\/pulls\/6(?:\?|$)/.test(url)) return Response.json({ number: 6, state: "open", mergeable_state: "clean", head: { sha: "a6" } }); - if (url.includes("/pulls/6/files")) return Response.json([]); - if (url.includes("/pulls/6/reviews")) return Response.json([]); - if (url.includes("/commits/a6/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/a6/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - return Response.json({}); - }); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "prime-pr-state-cache", repoFullName: "owner/agent-repo", prNumber: 6, installationId: 9001 }); - - expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 6)).toMatchObject({ - prMergeableState: "clean", - prState: "open", - }); - }); - }); -}); - -describe("enrichOpenPullRequestsWithChangedFiles (#2653)", () => { - const pr = (number: number, overrides: Partial = {}): PullRequestRecord => ({ - repoFullName: "owner/repo", - number, - title: `PR ${number}`, - state: "open", - labels: [], - linkedIssues: [], - ...overrides, - }); - - it("populates changedFiles for open PRs from the pull_request_files cache", async () => { - const env = createTestEnv(); - await upsertPullRequestFile(env, { repoFullName: "owner/repo", pullNumber: 10, path: "src/a.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); - await upsertPullRequestFile(env, { repoFullName: "owner/repo", pullNumber: 10, path: "src/b.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); - await upsertPullRequestFile(env, { repoFullName: "owner/repo", pullNumber: 11, path: "src/c.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); - - const result = await enrichOpenPullRequestsWithChangedFiles(env, "owner/repo", [pr(10), pr(11)]); - - expect(result.find((candidate) => candidate.number === 10)?.changedFiles?.sort()).toEqual(["src/a.ts", "src/b.ts"]); - expect(result.find((candidate) => candidate.number === 11)?.changedFiles).toEqual(["src/c.ts"]); - }); - - it("leaves a PR's changedFiles untouched when the cache has no rows for it (fail-safe degrade, not an error)", async () => { - const env = createTestEnv(); - await upsertPullRequestFile(env, { repoFullName: "owner/repo", pullNumber: 10, path: "src/a.ts", additions: 1, deletions: 0, changes: 1, payload: {} }); - - const result = await enrichOpenPullRequestsWithChangedFiles(env, "owner/repo", [pr(10), pr(12)]); - - expect(result.find((candidate) => candidate.number === 12)?.changedFiles).toBeUndefined(); - }); - - it("does not query the cache and returns the same array reference when there are no open PRs", async () => { - const env = createTestEnv(); - const input = [pr(20, { state: "closed" })]; - - const result = await enrichOpenPullRequestsWithChangedFiles(env, "owner/repo", input); - - expect(result).toBe(input); - }); - - it("returns the same array reference when the cache has no rows for any open PR", async () => { - const env = createTestEnv(); - const input = [pr(30)]; - - const result = await enrichOpenPullRequestsWithChangedFiles(env, "owner/repo", input); - - expect(result).toBe(input); - }); -}); - -describe("backlog-convergence sweep (#selfhost-backlog-convergence)", () => { - it("fans out to acting-autonomy repos, skipping a non-acting/non-allowlisted repo", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITTENSORY_REVIEW_REPOS: "", - JOBS: { async send(message: import("../../src/types").JobMessage) { sent.push(message); } } as unknown as Queue, - }); - await upsertRepositoryFromGitHub(env, { name: "agent-a", full_name: "owner/agent-a", private: false, owner: { login: "owner" } }); - await upsertRepositoryFromGitHub(env, { name: "plain-repo", full_name: "owner/plain-repo", private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-a", autonomy: { merge: "auto" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/plain-repo", autonomy: { review: "observe" } }); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); - - expect(sent).toHaveLength(1); - expect(sent[0]).toMatchObject({ type: "backlog-convergence-sweep", repoFullName: "owner/agent-a" }); - const fanout = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?") - .bind("agent.sweep.backlog_convergence.fanout") - .first<{ outcome: string; metadata_json: string }>(); - expect(fanout?.outcome).toBe("queued"); - expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 1, requestedBy: "schedule" }); - }); - - it("also fans out to an allowlisted repo regardless of autonomy mode (#sweep-all-modes parity)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ GITTENSORY_REVIEW_REPOS: "owner/advisory-repo", JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertRepositoryFromGitHub(env, { name: "advisory-repo", full_name: "owner/advisory-repo", private: false, owner: { login: "owner" } }, 9502); - await upsertRepositorySettings(env, { repoFullName: "owner/advisory-repo", autonomy: { merge: "observe" } }); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); - - expect(sent).toEqual([expect.objectContaining({ type: "backlog-convergence-sweep", repoFullName: "owner/advisory-repo", installationId: 9502 })]); - }); - - it("fans out to an allowlisted repo that was never registered locally (no installationId) and staggers a second repo's delay", async () => { - const sent: Array<{ message: import("../../src/types").JobMessage; delaySeconds?: number }> = []; - const env = createTestEnv({ - GITTENSORY_REVIEW_REPOS: "owner/never-registered", - JOBS: { async send(m: import("../../src/types").JobMessage, options?: { delaySeconds?: number }) { sent.push({ message: m, ...(options?.delaySeconds === undefined ? {} : { delaySeconds: options.delaySeconds }) }); } } as unknown as Queue, - }); - await upsertRepositoryFromGitHub(env, { name: "agent-a", full_name: "owner/agent-a", private: false, owner: { login: "owner" } }, 9506); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-a", autonomy: { merge: "auto" } }); - // owner/never-registered is allowlisted but has no local repository row at all -> no installationId to attach. - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); - - expect(sent).toHaveLength(2); - const neverRegistered = sent.find((s) => s.message.type === "backlog-convergence-sweep" && s.message.repoFullName === "owner/never-registered"); - expect(neverRegistered?.message).not.toHaveProperty("installationId"); - // Whichever entry landed second (index 1) carries a nonzero stagger delay. - expect(sent.some((s) => (s.delaySeconds ?? 0) > 0)).toBe(true); - }); - - it("no-ops safely on a missing repo arg or an un-configured repo", async () => { - const env = createTestEnv({}); - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test" }); - await upsertRepositoryFromGitHub(env, { name: "plain-repo", full_name: "owner/plain-repo", private: false, owner: { login: "owner" } }); - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/plain-repo" }); - - const count = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence").first<{ n: number }>(); - expect(count?.n).toBe(0); - }); - - it("respects the global pause kill-switch: a paused repo records a denial and enqueues nothing", async () => { - const env = createTestEnv({}); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9503); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, agentPaused: true }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale surface", state: "open", user: { login: "contributor" }, head: { sha: "abc" }, labels: [], body: "x" }); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const audit = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?") - .bind("agent.sweep.backlog_convergence") - .first<{ outcome: string; detail: string; metadata_json: string }>(); - expect(audit?.outcome).toBe("denied"); - expect(audit?.detail).toMatch(/paused/i); - expect(JSON.parse(audit?.metadata_json ?? "{}")).toMatchObject({ mode: "paused" }); - }); - - it("stays quiet (no audit, no enqueue) with no installation to act with", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }); // no installationId - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale surface", state: "open", user: { login: "contributor" }, head: { sha: "abc" }, labels: [], body: "x" }); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - expect(sent).toEqual([]); - const count = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence").first<{ n: number }>(); - expect(count?.n).toBe(0); - }); - - it("stays quiet when every open PR's surface is already published at its current head", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9504, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9504); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Converged", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "x" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 7, "a7"); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - expect(sent).toEqual([]); - }); - - it("fans out one agent-regate-pr per stale-surface candidate, tagged with the backlog-convergence deliveryId prefix", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9505, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9505); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - // #7 never had its surface published; #8 was published at an OLDER head than its current one; #9 is fully converged; - // #10 is a legacy/sparse row with no GitHub created_at, and still needs a re-gate without PR-age metadata. - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Never published", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "x", created_at: "2026-07-03T10:00:00.000Z" }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 8, title: "Stale surface", state: "open", user: { login: "contributor" }, head: { sha: "b8" }, labels: [], body: "x", created_at: "2026-07-03T11:00:00.000Z" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 8, "old-b8"); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 9, title: "Converged", state: "open", user: { login: "contributor" }, head: { sha: "a9" }, labels: [], body: "x" }); - await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 9, "a9"); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 10, title: "Sparse legacy row", state: "open", user: { login: "contributor" }, head: { sha: "a10" }, labels: [], body: "x" }); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule", repoFullName: "owner/agent-repo" }); - - const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); - expect(fanned.map((job) => job.prNumber).sort((a, b) => a - b)).toEqual([7, 8, 10]); - for (const job of fanned) { - expect(job.deliveryId).toBe(`backlog-convergence:owner/agent-repo#${job.prNumber}`); - expect(job.installationId).toBe(9505); - } - expect(Object.fromEntries(fanned.map((job) => [job.prNumber, job.prCreatedAt]))).toEqual({ - 7: "2026-07-03T10:00:00.000Z", - 8: "2026-07-03T11:00:00.000Z", - 10: undefined, - }); - const audit = await env.DB.prepare("select outcome, detail, metadata_json from audit_events where event_type = ?") - .bind("agent.sweep.backlog_convergence") - .first<{ outcome: string; detail: string; metadata_json: string }>(); - expect(audit?.outcome).toBe("completed"); - const meta = JSON.parse(audit?.metadata_json ?? "{}"); - expect(meta).toMatchObject({ repoFullName: "owner/agent-repo", openCount: 4, examined: 3 }); - expect(meta.candidatePulls.sort((a: number, b: number) => a - b)).toEqual([7, 8, 10]); - }); - - it("REGRESSION (#4502, #audit-sweep-dispatch-stamp): ONE sweep stamps ALL candidates AT DISPATCH, so the next fan-out skips the repo as draining — no overlapping sweeps", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9510, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9510); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - for (const number of [7, 8, 9]) { - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number, title: `PR${number}`, state: "open", user: { login: "c" }, head: { sha: `a${number}` }, labels: [], body: "" }); - } - - // Run ONE per-repo sweep — do NOT drain the per-PR jobs (simulate the staggered re-reviews not having run yet). - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - // The marker is stamped for EVERY candidate immediately at dispatch — NOT waiting on the per-PR jobs. - const stamped = await env.DB.prepare("select count(*) as n from pull_requests where repo_full_name = ? and last_backlog_convergence_regated_at is not null").bind("owner/agent-repo").first<{ n: number }>(); - expect(stamped?.n).toBe(3); - - // So the very next cron fan-out sees the fresh stamp and SKIPS this repo as draining — the overlap that would - // duplicate per-PR jobs is gone. - sent.length = 0; - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); - expect(sent.some((m) => m.type === "backlog-convergence-sweep" && m.repoFullName === "owner/agent-repo")).toBe(false); - const fanout = await env.DB.prepare("select metadata_json from audit_events where event_type = ? order by created_at desc limit 1").bind("agent.sweep.backlog_convergence.fanout").first<{ metadata_json: string }>(); - expect(JSON.parse(fanout?.metadata_json ?? "{}").skippedDraining).toBeGreaterThanOrEqual(1); - }); - - it("INVARIANT (#4502, in-flight guard): the fan-out SKIPS a repo whose prior sweep is still draining, enqueues an idle one", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ GITTENSORY_REVIEW_REPOS: "", JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9511, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - for (const name of ["draining", "idle"]) { - await upsertRepositoryFromGitHub(env, { name, full_name: `owner/${name}`, private: false, owner: { login: "owner" } }, 9511); - await upsertRepositorySettings(env, { repoFullName: `owner/${name}`, autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, `owner/${name}`, { number: 1, title: "PR1", state: "open", user: { login: "c" }, head: { sha: "h1" }, labels: [], body: "" }); - } - // owner/draining was just backlog-convergence-regated (a sweep is mid-drain); owner/idle has never been swept. - await repositoriesModule.markPullRequestsBacklogConvergenceRegated(env, "owner/draining", [1]); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); // no repoFullName → fan-out path - - const sweepRepos = sent.filter((m): m is Extract => m.type === "backlog-convergence-sweep").map((m) => m.repoFullName); - expect(sweepRepos).toEqual(["owner/idle"]); // the draining repo is skipped, the idle one enqueued - const fanout = await env.DB.prepare("select metadata_json from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence.fanout").first<{ metadata_json: string }>(); - expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 1, skippedDraining: 1 }); - }); - - it("INVARIANT (#4502, #audit-fanout-dedup): a BURST of fan-outs collapses to ONE — the second claims nothing and audits denied", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9512, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9512); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "PR7", state: "open", user: { login: "c" }, head: { sha: "a7" }, labels: [], body: "" }); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); // first fan-out claims the window - expect(sent.some((m) => m.type === "backlog-convergence-sweep" && m.repoFullName === "owner/agent-repo")).toBe(true); - - sent.length = 0; - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); // burst sibling in the same window → deduped - expect(sent.filter((m) => m.type === "backlog-convergence-sweep")).toEqual([]); // enqueues no redundant sweep - const denied = await env.DB.prepare("select count(*) as n from audit_events where event_type='agent.sweep.backlog_convergence.fanout' and outcome='denied'").first<{ n: number }>(); - expect(denied?.n).toBe(1); - }); - - it("REGRESSION (#4502, #audit-sweep-fanout-isolation): one repo's settings-check failure does not abort the fan-out for every other repo", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITTENSORY_REVIEW_REPOS: "", - JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue, - }); - await upsertRepositoryFromGitHub(env, { name: "agent-a", full_name: "owner/agent-a", private: false, owner: { login: "owner" } }); - await upsertRepositoryFromGitHub(env, { name: "agent-b", full_name: "owner/agent-b", private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-a", autonomy: { label: "auto" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-b", autonomy: { label: "auto" } }); - const realResolve = repositorySettingsModule.resolveRepositorySettings; - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - const resolveSpy = vi.spyOn(repositorySettingsModule, "resolveRepositorySettings").mockImplementation(async (e, repoFullName) => { - if (repoFullName === "owner/agent-a") throw new Error("D1 read error"); - return realResolve(e, repoFullName); - }); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); - - expect(sent).toEqual([expect.objectContaining({ type: "backlog-convergence-sweep", repoFullName: "owner/agent-b" })]); // agent-a's failure did not block agent-b - expect(errors.mock.calls.some((call) => String(call[0]).includes("backlog_convergence_fanout_repo_check_failed") && String(call[0]).includes("owner/agent-a"))).toBe(true); - const fanout = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence.fanout").first<{ outcome: string; metadata_json: string }>(); - expect(fanout?.outcome).toBe("queued"); // the fan-out still completes and records its own outcome - expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 1, skippedErrored: 1 }); - errors.mockRestore(); - resolveSpy.mockRestore(); - }); - - it("REGRESSION (#4502, #audit-sweep-fanout-isolation): one repo's dispatch failure does not abort dispatch for every other repo, and the fan-out audit event still records", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITTENSORY_REVIEW_REPOS: "", - JOBS: { - async send(m: import("../../src/types").JobMessage) { - if (m.type === "backlog-convergence-sweep" && m.repoFullName === "owner/agent-a") throw new Error("queue send error"); - sent.push(m); - }, - } as unknown as Queue, - }); - await upsertRepositoryFromGitHub(env, { name: "agent-a", full_name: "owner/agent-a", private: false, owner: { login: "owner" } }); - await upsertRepositoryFromGitHub(env, { name: "agent-b", full_name: "owner/agent-b", private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-a", autonomy: { label: "auto" } }); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-b", autonomy: { label: "auto" } }); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); - - expect(sent).toEqual([expect.objectContaining({ type: "backlog-convergence-sweep", repoFullName: "owner/agent-b" })]); // agent-a's failed send did not block agent-b's - expect(errors.mock.calls.some((call) => String(call[0]).includes("backlog_convergence_fanout_dispatch_failed") && String(call[0]).includes("owner/agent-a"))).toBe(true); - const fanout = await env.DB.prepare("select outcome, metadata_json from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence.fanout").first<{ outcome: string; metadata_json: string }>(); - expect(fanout?.outcome).toBe("queued"); // reached — the dispatch failure did not throw the fan-out itself - expect(JSON.parse(fanout?.metadata_json ?? "{}")).toMatchObject({ repoCount: 2 }); // both PASSED their settings/draining checks regardless of dispatch outcome - errors.mockRestore(); - }); - - it("agent re-gate sweep swallows a failing last_backlog_convergence_regated_at stamp and still completes (#4502, #audit-sweep-converge)", async () => { - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue }); - await upsertInstallation(env, { action: "created", installation: { id: 9513, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9513); - await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" } }); - await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 7, title: "Stale surface", state: "open", user: { login: "contributor" }, head: { sha: "a7" }, labels: [], body: "" }); - const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - const stamp = vi.spyOn(repositoriesModule, "markPullRequestsBacklogConvergenceRegated").mockRejectedValueOnce(new Error("D1 write error")); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - const audit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.sweep.backlog_convergence").first<{ outcome: string }>(); - expect(audit?.outcome).toBe("completed"); // the sweep still completes; the dispatch-time stamp failure is swallowed - expect(sent.some((m) => m.type === "agent-regate-pr" && m.prNumber === 7)).toBe(true); // the per-PR fan-out still happens - expect(errors.mock.calls.some((call) => String(call[0]).includes("backlog_convergence_mark_regated_failed"))).toBe(true); - stamp.mockRestore(); - errors.mockRestore(); - }); - - it("REGRESSION (#4502, #3899-style port): resolves multiple repos' settings/drain-state CONCURRENTLY, bounded by SWEEP_FANOUT_RESOLUTION_CONCURRENCY, and drops no repo", async () => { - vi.useRealTimers(); - const sent: import("../../src/types").JobMessage[] = []; - const env = createTestEnv({ - GITTENSORY_REVIEW_REPOS: "", - JOBS: { async send(m: import("../../src/types").JobMessage) { sent.push(m); } } as unknown as Queue, - }); - const repoNames = ["r1", "r2", "r3", "r4", "r5", "r6"]; - for (const name of repoNames) { - await upsertRepositoryFromGitHub(env, { name, full_name: `owner/${name}`, private: false, owner: { login: "owner" } }); - await upsertRepositorySettings(env, { repoFullName: `owner/${name}`, autonomy: { merge: "auto" } }); - } - const { mapWithConcurrencyLimit: realMapWithConcurrencyLimit } = - await vi.importActual("../../src/signals/focus-manifest-loader"); - let inFlight = 0; - let maxInFlight = 0; - const mapSpy = vi.spyOn(focusManifestLoaderModule, "mapWithConcurrencyLimit").mockImplementation( - async (items, limit, mapper) => { - expect(limit).toBe(SWEEP_FANOUT_RESOLUTION_CONCURRENCY); - return realMapWithConcurrencyLimit(items, limit, async (item) => { - inFlight += 1; - maxInFlight = Math.max(maxInFlight, inFlight); - try { - await new Promise((resolve) => setTimeout(resolve, 5)); // hold the window open long enough for others to overlap - return await mapper(item); - } finally { - inFlight -= 1; - } - }); - }, - ); - - await processJob(env, { type: "backlog-convergence-sweep", requestedBy: "schedule" }); - - expect(mapSpy).toHaveBeenCalled(); - expect(maxInFlight).toBeGreaterThan(1); // proves real overlap — not the old strictly-sequential loop - expect(maxInFlight).toBeLessThanOrEqual(SWEEP_FANOUT_RESOLUTION_CONCURRENCY); // proves BOUNDED, not unlimited fan-out - expect(sent.filter((m) => m.type === "backlog-convergence-sweep").length).toBe(repoNames.length); // every repo still dispatched, none silently dropped - }); -}); - -// #selfhost-auto-action-convergence: end-to-end regression coverage for the GENERAL heuristic plan+execute path -// (runAgentMaintenancePlanAndExecute -> planAgentMaintenanceActions -> executeAgentMaintenanceActions), via real -// webhook -> processJob -> mocked-GitHub-API assertions. The specialized short-circuit mechanisms (blacklist, -// contributor-cap, review-nag, converted_to_draft gate-close) already have deep end-to-end coverage elsewhere in -// this file; planAgentMaintenanceActions itself is exhaustively unit-tested in agent-actions.test.ts; and -// executeAgentMaintenanceActions's own gate stack is exhaustively unit-tested in agent-action-executor.test.ts. -// What was missing was END-TO-END proof, for the plain gate-verdict path specifically, that the two connect: a -// plan computed from REAL PR/settings state actually reaches a REAL (mocked) GitHub mutation. -describe("auto-action convergence: end-to-end plan+execute for the general heuristic path (#selfhost-auto-action-convergence)", () => { - const REPO = "JSONbored/gittensory"; - const INSTALLATION_ID = 9600; - - beforeEach(() => clearInstallationTokenCacheForTest()); - afterEach(() => { - clearInstallationTokenCacheForTest(); - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - }); - - async function setupAutoActionRepo(env: ReturnType, settingsOverrides: Record = {}): Promise { - await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }, INSTALLATION_ID); - await upsertInstallation(env, { - installation: { - id: INSTALLATION_ID, - account: { login: "JSONbored", id: 1, type: "User" }, - repository_selection: "selected", - permissions: { metadata: "read", contents: "write", pull_requests: "write", issues: "write" }, - events: ["pull_request"], - }, - repositories: [{ name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: REPO, - commentMode: "off", - publicSurface: "off", - checkRunMode: "off", - gateCheckMode: "enabled", reviewCheckMode: "required", - linkedIssueGateMode: "block", // the default blocker mechanism for these tests: missing linked issue -> gate failure - ...settingsOverrides, - }); - // Without a registry snapshot the gate reports a "repo_unregistered" warning finding, which keeps the - // conclusion at "neutral" instead of "success"/"failure" -- register the repo so the tests below exercise - // real merge/close dispositions rather than the not-evaluated-yet state. - await persistRegistrySnapshot( - env, - normalizeRegistryPayload({ [REPO]: { emission_share: 0.01, issue_discovery_share: 0 } }, { kind: "raw-github", url: "https://example.test" }, "2026-05-23T00:00:00.000Z"), - ); - } - - function prPayload(overrides: Record = {}): Record { - return { - action: "opened", - installation: { id: INSTALLATION_ID, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }, - pull_request: { - number: 60, - title: "A PR", - state: "open", - user: { login: "contributor" }, - head: { sha: "conv60" }, - labels: [], - body: "no linked issue here", // missing-linked-issue -> gate conclusion=failure under linkedIssueGateMode:block - mergeable_state: "clean", - reviewDecision: "APPROVED", - ...overrides, - }, - }; - } - - /** A fetch stub for one PR (number/head parametrized) with a controllable CI state, capturing whether a real - * merge (PUT .../pulls/N/merge) or close (PATCH .../pulls/N with state:"closed") mutation actually fired. */ - function stubPrFetch( - prNumber: number, - headSha: string, - seen: { closed: boolean; merged: boolean }, - ciState: "clear" | "pending" | "passed" = "clear", - ): void { - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url === "https://api.github.com/graphql") { - return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - } - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([]); - if (url.includes(`/pulls/${prNumber}/reviews`)) return Response.json([]); - if (url.includes(`/pulls/${prNumber}/commits`)) return Response.json([]); - if (url.endsWith(`/pulls/${prNumber}/merge`) && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true }); - } - if (url.endsWith(`/pulls/${prNumber}`) && method === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")); - if (body.state === "closed") seen.closed = true; - return Response.json({ number: prNumber, state: body.state ?? "open" }); - } - if (url.endsWith(`/pulls/${prNumber}`)) { - return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: headSha }, mergeable_state: "clean" }); - } - if (url.includes(`/commits/${headSha}/check-runs`)) { - if (ciState === "pending") return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "in_progress", conclusion: null, app: { slug: "github-actions" } }] }); - if (ciState === "passed") return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - return Response.json({ total_count: 0, check_runs: [] }); - } - if (url.includes(`/commits/${headSha}/status`)) { - return Response.json({ state: ciState === "pending" ? "pending" : "success", statuses: [] }); - } - if (url.includes(`/issues/${prNumber}/labels`)) return Response.json([]); - if (url.includes(`/issues/${prNumber}/comments`)) return Response.json([]); - return Response.json({}); - }); - } - - it("REGRESSION: a blocked contributor PR (plain gate failure) with close=auto is actually closed via the general heuristic-close path", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { autonomy: { close: "auto" } }); - const seen = { closed: false, merged: false }; - stubPrFetch(60, "conv60", seen); - resetMetrics(); - - await processJob(env, { type: "github-webhook", deliveryId: "conv-close", eventName: "pull_request", payload: prPayload() }); - - expect(seen.closed).toBe(true); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - // #terminal-outcome-audit: the disposition counter's "close" action_class, with the actual gate-blocker - // code (missing_linked_issue, from the default linkedIssueGateMode:block + no-linked-issue body) as the - // bounded blocker_class -- proof this reaches the real gate.blockers, not just a hardcoded label. - expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="close",autonomy_level="auto",blocker_class="missing_linked_issue",repo="redacted-1"} 1'); - const nativeDecision = await env.DB.prepare("select decision, summary, source from review_audit where event_type = 'gate_decision' and target_id = ?").bind(`${REPO}#60`).first<{ decision: string; summary: string; source: string }>(); - expect(nativeDecision).toMatchObject({ decision: "close", summary: "missing_linked_issue", source: "gittensory-native" }); - }); - - // REGRESSION (gate-flagged gap, #terminal-outcome-audit): a PR that touches a guardrail-protected path (e.g. - // .github/workflows/**) is otherwise clean, so the gate lands on conclusion:"neutral" via guardrailHit -- - // gate.blockers is empty for that conclusion (see evaluateGateCheckCore), so before this fix the disposition - // metric's blocker_class silently read "none", indistinguishable from a clean PR held on nothing more than - // pending CI. neutralHoldReasonCode recovers the real reason from gate.warnings instead. - it("a guardrail-path hold (neutral gate conclusion) records blocker_class=guardrail_hold, not 'none'", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { autonomy: { merge: "auto", close: "auto" }, linkedIssueGateMode: "off" }); - const seen = { closed: false, merged: false }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url === "https://raw.githubusercontent.com/JSONbored/gittensory/HEAD/.gittensory.yml") return new Response("settings:\n hardGuardrailGlobs:\n - .github/workflows/**\n"); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/61/files")) return Response.json([{ filename: ".github/workflows/ci.yml", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+ x: 1" }]); - if (url.includes("/pulls/61/reviews")) return Response.json([]); - if (url.includes("/pulls/61/commits")) return Response.json([]); - if (url.endsWith("/pulls/61/merge") && method === "PUT") { seen.merged = true; return Response.json({ merged: true }); } - if (url.endsWith("/pulls/61") && method === "PATCH") { - const body = JSON.parse(String(init?.body ?? "{}")); - if (body.state === "closed") seen.closed = true; - return Response.json({ number: 61, state: body.state ?? "open" }); - } - if (url.endsWith("/pulls/61")) return Response.json({ number: 61, state: "open", user: { login: "contributor" }, head: { sha: "conv61" }, mergeable_state: "clean" }); - if (url.includes("/commits/conv61/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/commits/conv61/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/61/labels")) return Response.json([]); - if (url.includes("/issues/61/comments")) return Response.json([]); - return Response.json({}); - }); - resetMetrics(); - - await processJob(env, { type: "github-webhook", deliveryId: "conv-guardrail", eventName: "pull_request", payload: prPayload({ number: 61, head: { sha: "conv61" }, body: "no linked issue needed" }) }); - - expect(seen.merged).toBe(false); - expect(seen.closed).toBe(false); - expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="hold",autonomy_level="auto",blocker_class="guardrail_hold",repo="redacted-1"} 1'); - const holdAudit = await env.DB.prepare("select metadata_json from audit_events where event_type = 'agent.action.hold' order by created_at desc limit 1").first<{ metadata_json: string }>(); - expect(JSON.parse(holdAudit?.metadata_json ?? "{}")).toMatchObject({ - repoFullName: REPO, - pullNumber: 61, - disposition: { actionClass: "hold", blockerClass: "guardrail_hold" }, - guardrailMatches: [{ path: ".github/workflows/ci.yml", glob: ".github/workflows/**" }], - }); - }); - - it("reviewCheckMode: disabled still auto-closes a blocked contributor PR via the general heuristic-close path (#2852)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { autonomy: { close: "auto" }, reviewCheckMode: "disabled" }); - const seen = { closed: false, merged: false }; - let checkRunApiCalls = 0; - stubPrFetch(66, "conv66", seen); - const realFetch = globalThis.fetch; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; - return realFetch(input, init); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-disabled-close", - eventName: "pull_request", - payload: prPayload({ number: 66, head: { sha: "conv66" } }), - }); - - expect(seen.closed).toBe(true); - expect(checkRunApiCalls).toBe(0); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBeGreaterThanOrEqual(1); - }); - - it("REGRESSION: a green-verdict PR with CI still pending is NOT merged (merge withheld until CI/mergeability settle)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off" }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - const seen = { closed: false, merged: false }; - stubPrFetch(61, "conv61", seen, "pending"); - - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-ci-pending", - eventName: "pull_request", - payload: prPayload({ number: 61, head: { sha: "conv61" }, body: "Closes #1" }), - }); - - expect(seen.merged).toBe(false); - const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); - expect(mergeAudit?.n).toBe(0); - }); - - it("REGRESSION (#selfhost-backlog-convergence): a CI-pending PR defers, then merges once check_suite.completed reports CI green (convergence chain)", async () => { - // maybeReReviewOnCiCompletion (processors.ts) gates its ENTIRE re-review loop on isConvergenceRepoAllowed - // (the GITTENSORY_REVIEW_REPOS cutover allowlist), independent of autonomy -- the check_suite/check_run - // "THE auto-merge trigger" path only fires for an allowlisted repo. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_REPOS: REPO }); - await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off" }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - const seen = { closed: false, merged: false }; - let ciState: "pending" | "passed" = "pending"; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - // Delegate to a fresh stub per call so the closure sees the CURRENT ciState -- stubPrFetch captures ciState - // by value at call time, so re-invoke its logic inline against the live ciState variable instead. - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url === "https://api.github.com/graphql") { - return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - } - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - // A non-empty, non-guardrail file: an EMPTY files list is treated as "unresolved" and fails CLOSED into a - // guardrail hold (isGuardrailHit short-circuits true on changedPaths.length === 0) -- so this must return a - // real file for the merge disposition below to ever reach a genuine "success" gate conclusion. - if (url.includes("/pulls/62/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.includes("/pulls/62/reviews")) return Response.json([]); - if (url.includes("/pulls/62/commits")) return Response.json([]); - if (url.endsWith("/pulls/62/merge") && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true }); - } - if (url.endsWith("/pulls/62")) { - return Response.json({ number: 62, state: "open", user: { login: "contributor" }, head: { sha: "conv62" }, mergeable_state: "clean" }); - } - if (url.includes("/commits/conv62/check-runs")) { - return ciState === "pending" - ? Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "in_progress", conclusion: null, app: { slug: "github-actions" } }] }) - : Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - } - if (url.includes("/commits/conv62/status")) return Response.json({ state: ciState === "pending" ? "pending" : "success", statuses: [] }); - if (url.includes("/issues/62/labels")) return Response.json([]); - if (url.includes("/issues/62/comments")) return Response.json([]); - return Response.json({}); - }); - - // Step 1: a synchronize webhook while CI is still running -> merge withheld. - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-chain-1", - eventName: "pull_request", - payload: prPayload({ number: 62, head: { sha: "conv62" }, body: "Closes #1", action: "synchronize" }), - }); - expect(seen.merged).toBe(false); - - // Step 2: CI finishes; a check_suite.completed webhook for the SAME head re-triggers the pipeline, which now - // sees a passing CI aggregate and merges. - ciState = "passed"; - resetMetrics(); - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-chain-2", - eventName: "check_suite", - payload: { - action: "completed", - installation: { id: INSTALLATION_ID, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }, - check_suite: { head_sha: "conv62", conclusion: "success", pull_requests: [{ number: 62 }] }, - } as never, - }); - - expect(seen.merged).toBe(true); - const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); - expect(mergeAudit?.n).toBeGreaterThanOrEqual(1); - // #terminal-outcome-audit: the disposition counter's "merge" action_class, on the actual live call site. - expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="merge",autonomy_level="auto",blocker_class="none",repo="redacted-1"} 1'); - }); - - // #terminal-outcome-audit: end-to-end proof that the LIVE runAgentMaintenancePlanAndExecute call site (not just - // the extracted pure precisionBreakerDowngradeDirections/applyPrecisionBreakers unit tests) actually increments - // gittensory_precision_breaker_downgrades_total when an engaged accuracy circuit-breaker rewrites a real plan. - it("REGRESSION (#terminal-outcome-audit): an engaged holdonly breaker withholds a real would-merge AND increments the downgrade counter", async () => { - // Mirrors the "#selfhost-backlog-convergence" chain test above (same two-step CI-pending-then-green shape, - // the proven way this suite reaches a REAL merge attempt): a plain "opened" webhook with CI already green - // never reaches the merge decision in this harness; the check_suite.completed re-review path does. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_REPOS: REPO }); - await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off" }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - const seen = { closed: false, merged: false }; - let ciState: "pending" | "passed" = "pending"; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/access_tokens")) return Response.json({ token: "test-token" }); - if (url.includes("/pulls/65/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.includes("/pulls/65/reviews")) return Response.json([]); - if (url.includes("/pulls/65/commits")) return Response.json([]); - if (url.endsWith("/pulls/65/merge") && method === "PUT") { seen.merged = true; return Response.json({ merged: true }); } - if (url.endsWith("/pulls/65")) return Response.json({ number: 65, state: "open", user: { login: "contributor" }, head: { sha: "conv65" }, mergeable_state: "clean" }); - if (url.includes("/commits/conv65/check-runs")) { - return ciState === "pending" - ? Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "in_progress", conclusion: null, app: { slug: "github-actions" } }] }) - : Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - } - if (url.includes("/commits/conv65/status")) return Response.json({ state: ciState === "pending" ? "pending" : "success", statuses: [] }); - if (url.includes("/issues/65/labels")) return Response.json([]); - if (url.includes("/issues/65/comments")) return Response.json([]); - return Response.json({}); - }); - - // Step 1: a synchronize webhook while CI is still running — establishes the PR, no merge yet. - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-holdonly-1", - eventName: "pull_request", - payload: prPayload({ number: 65, head: { sha: "conv65" }, body: "Closes #1", action: "synchronize" }), - }); - expect(seen.merged).toBe(false); - - // Engage the merge-precision breaker for this exact repo BEFORE CI resolves — mirrors how runSelfTuneBreaker - // (or a human) would set it via system_flags ahead of the next re-review. - await env.DB.prepare("INSERT OR REPLACE INTO system_flags (key, value, updated_at) VALUES ('holdonly:JSONbored/gittensory', '1', CURRENT_TIMESTAMP)").run(); - resetMetrics(); - - // Step 2: CI finishes; a check_suite.completed webhook re-triggers the pipeline — without the breaker this - // would merge exactly like the sibling convergence-chain test above; the engaged breaker withholds it instead. - ciState = "passed"; - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-holdonly-2", - eventName: "check_suite", - payload: { - action: "completed", - installation: { id: INSTALLATION_ID, account: { login: "JSONbored", id: 1, type: "User" } }, - repository: { name: "gittensory", full_name: REPO, private: false, owner: { login: "JSONbored" } }, - check_suite: { head_sha: "conv65", conclusion: "success", pull_requests: [{ number: 65 }] }, - } as never, - }); - - expect(seen.merged).toBe(false); - const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); - expect(mergeAudit?.n).toBe(0); - expect(await renderMetrics()).toContain('gittensory_precision_breaker_downgrades_total{direction="merge"} 1'); - // #terminal-outcome-audit: the ALWAYS-recorded disposition counter, placed before the "nothing was planned" - // early return -- this is the exact "hold, but no audit_events row at all" shape (the breaker downgrade - // leaves no merge/close action) that previously had zero aggregate signal. close autonomy is unset in this - // repo's settings (only merge/approve are configured), so it resolves to the default "observe". - expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="hold",autonomy_level="observe",blocker_class="none",repo="redacted-1"} 1'); - const holdAudit = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = 'agent.action.hold' order by created_at desc limit 1").first<{ detail: string; metadata_json: string }>(); - expect(holdAudit?.detail).toBe("auto-action held by precision circuit breaker"); - expect(JSON.parse(holdAudit?.metadata_json ?? "{}")).toMatchObject({ - repoFullName: REPO, - pullNumber: 65, - gateConclusion: "success", - ciState: "passed", - disposition: { actionClass: "hold", blockerClass: "none" }, - plannedActionClasses: ["merge"], - finalActionClasses: ["label"], - }); - }); - - it("reviewCheckMode: disabled still auto-merges a green PR via the general heuristic path, with ZERO check-run API calls (#2852)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off", reviewCheckMode: "disabled" }); - await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000); - const seen = { closed: false, merged: false }; - let checkRunApiCalls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/64/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.includes("/pulls/64/reviews")) return Response.json([]); - if (url.includes("/pulls/64/commits")) return Response.json([]); - if (url.endsWith("/pulls/64/merge") && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true }); - } - if (url.endsWith("/pulls/64")) return Response.json({ number: 64, state: "open", user: { login: "contributor" }, head: { sha: "conv64" }, mergeable_state: "clean" }); - if (url.includes("/commits/conv64/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/commits/conv64/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/64/labels")) return Response.json([]); - if (url.includes("/issues/64/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-disabled-merge", - eventName: "pull_request", - payload: prPayload({ number: 64, head: { sha: "conv64" }, body: "Closes #1" }), - }); - - expect(seen.merged).toBe(true); - expect(checkRunApiCalls).toBe(0); - const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); - expect(mergeAudit?.n).toBeGreaterThanOrEqual(1); - }); - - it("reviewCheckMode: disabled still auto-merges an AUTHOR-LESS (ghost) PR when autonomy is configured (#2852)", async () => { - // A ghost PR (no `user` at all -> authorLogin null) is the one other early-return in - // maybePublishPrPublicSurface gated on gateEnabled (`if (!author && !gateEnabled && !autonomyNeedsGateEvaluation) - // return undefined;`) -- proves autonomyNeedsGateEvaluation also keeps THIS guard from bailing. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { autonomy: { merge: "auto", approve: "auto" }, linkedIssueGateMode: "off", reviewCheckMode: "disabled" }); - const seen = { closed: false, merged: false }; - let checkRunApiCalls = 0; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/67/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.includes("/pulls/67/reviews")) return Response.json([]); - if (url.includes("/pulls/67/commits")) return Response.json([]); - if (url.endsWith("/pulls/67/merge") && method === "PUT") { - seen.merged = true; - return Response.json({ merged: true }); - } - if (url.endsWith("/pulls/67")) return Response.json({ number: 67, state: "open", head: { sha: "conv67" }, mergeable_state: "clean" }); - if (url.includes("/commits/conv67/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/commits/conv67/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/67/labels")) return Response.json([]); - if (url.includes("/issues/67/comments")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-disabled-ghost-author", - eventName: "pull_request", - payload: prPayload({ number: 67, head: { sha: "conv67" }, body: "Closes #1", user: undefined }), - }); - - expect(seen.merged).toBe(true); - expect(checkRunApiCalls).toBe(0); - }); - - it("an author-less (ghost) PR with the check-run disabled and NO autonomy configured stays fully silent (early-return preserved)", async () => { - // Mirrors the ghost-PR test above but WITHOUT autonomy configured -- proves the early return in - // maybePublishPrPublicSurface still fires (bails to undefined, no work at all) when neither gateEnabled nor - // autonomyNeedsGateEvaluation applies, exactly as before #2852. - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { reviewCheckMode: "disabled" }); // autonomy defaults to {} (unconfigured) - let checkRunApiCalls = 0; - let mergeAttempted = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; - if (url.endsWith("/pulls/68/merge")) mergeAttempted = true; - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "ghost-no-autonomy", - eventName: "pull_request", - payload: prPayload({ number: 68, head: { sha: "conv68" }, body: "no linked issue here", user: undefined }), - }); - - expect(checkRunApiCalls).toBe(0); - expect(mergeAttempted).toBe(false); - }); - - it("reviewCheckMode: disabled still posts the sticky PR comment and label (public surface is independent of the check-run publish decision) (#2852)", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { - autonomy: { merge: "auto", approve: "auto" }, - linkedIssueGateMode: "off", - reviewCheckMode: "disabled", - commentMode: "all_prs", - publicSurface: "comment_and_label", - }); - let checkRunApiCalls = 0; - let commentPosted = false; - let labelApplied = false; - vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { - const url = input.toString(); - const method = init?.method ?? "GET"; - if (/\/check-runs(?:\/|\?|$)/.test(url) && (method === "POST" || method === "PATCH")) checkRunApiCalls += 1; - if (url === "https://api.gittensor.io/miners") return Response.json([]); - if (url === "https://api.github.com/graphql") return Response.json({ data: { repository: { pullRequest: { reviewDecision: "APPROVED" } } } }); - if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/pulls/65/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]); - if (url.includes("/pulls/65/reviews")) return Response.json([]); - if (url.includes("/pulls/65/commits")) return Response.json([]); - if (url.endsWith("/pulls/65")) return Response.json({ number: 65, state: "open", user: { login: "contributor" }, head: { sha: "conv65" }, mergeable_state: "clean" }); - if (url.includes("/commits/conv65/check-runs")) return Response.json({ total_count: 1, check_runs: [{ name: "CI", status: "completed", conclusion: "success", app: { slug: "github-actions" } }] }); - if (url.includes("/commits/conv65/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/issues/65/comments") && method === "POST") { - commentPosted = true; - return Response.json({ id: 1 }); - } - if (url.includes("/issues/65/labels") && method === "POST") { - labelApplied = true; - return Response.json([]); - } - if (url.includes("/issues/65/comments") || url.includes("/issues/65/labels")) return Response.json([]); - return Response.json({}); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-disabled-surface", - eventName: "pull_request", - payload: prPayload({ number: 65, head: { sha: "conv65" }, body: "Closes #1" }), - }); - - expect(checkRunApiCalls).toBe(0); - expect(commentPosted).toBe(true); - expect(labelApplied).toBe(true); - }); - - it("REGRESSION: closeOwnerAuthors=false (default) protects an owner-authored blocked PR from the general heuristic-close path", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { autonomy: { close: "auto" } }); // closeOwnerAuthors defaults false - const seen = { closed: false, merged: false }; - stubPrFetch(63, "conv63", seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-owner-protected", - eventName: "pull_request", - payload: prPayload({ number: 63, head: { sha: "conv63" }, user: { login: "JSONbored" } }), // author = repo owner - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBe(0); - // Enriched hold-audit fields (#selfhost-holdplan-audit): this scenario's gate blocker (missing linked issue) - // already produced a specific "protected author" detail before this change -- what's new here is that - // `metadata` now ALSO carries the structured closeEligible/closeAutonomy/protectedAuthor fields, so a hold - // is debuggable from the audit table alone. The actual bug fix -- a RED-CI hold (no gate blocker at all) - // gaining the same protected-author/close-autonomy disambiguation the gate-blocker branch already had -- - // is unit-tested directly against agentHoldAuditDetail in precision-breakers-chain.test.ts, where the two - // branches can be exercised independently without needing a webhook fixture that produces CI-failed with - // zero gate blockers. - const holdAudit = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = 'agent.action.hold' order by created_at desc limit 1").first<{ detail: string; metadata_json: string }>(); - expect(holdAudit?.detail).toBe("close withheld for protected author on gate blocker missing_linked_issue"); - expect(JSON.parse(holdAudit?.metadata_json ?? "{}")).toMatchObject({ - repoFullName: "JSONbored/gittensory", - pullNumber: 63, - closeEligible: false, - closeAutonomy: "auto", - // The repo owner is also treated as an admin (GitHub's own collaborator-permission model), so both flags - // are true for this fixture -- only `automation` is meaningfully independent of `owner` here. - protectedAuthor: { owner: true, admin: true, automation: false }, - closeOwnerAuthors: false, - }); - }); - - it("REGRESSION: closeOwnerAuthors=true allows the general heuristic-close path to close a blocked owner-authored PR", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await setupAutoActionRepo(env, { autonomy: { close: "auto" }, closeOwnerAuthors: true }); - const seen = { closed: false, merged: false }; - stubPrFetch(64, "conv64", seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-owner-allowed", - eventName: "pull_request", - payload: prPayload({ number: 64, head: { sha: "conv64" }, user: { login: "JSONbored" } }), - }); - - expect(seen.closed).toBe(true); - }); - - it("REGRESSION (#2133): an ADMIN_GITHUB_LOGINS fleet-operator author is exempt from the general heuristic-close path, same as the literal repo owner", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), ADMIN_GITHUB_LOGINS: "admin-user" }); - await setupAutoActionRepo(env, { autonomy: { close: "auto" } }); // closeOwnerAuthors defaults false - const seen = { closed: false, merged: false }; - stubPrFetch(65, "conv65", seen); - - await processJob(env, { - type: "github-webhook", - deliveryId: "conv-admin-protected", - eventName: "pull_request", - payload: prPayload({ number: 65, head: { sha: "conv65" }, user: { login: "admin-user" } }), - }); - - expect(seen.closed).toBe(false); - const closeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.close'").first<{ n: number }>(); - expect(closeAudit?.n).toBe(0); - }); -}); - -// #automation-bot-skip: waste elimination for known automation authors (release-please's github-actions[bot], -// Renovate, Dependabot). End-to-end wiring on top of automation-bot-skip.test.ts's pure-function coverage -- -// these pin the webhook + re-entry integration points, including the SECURITY property that a human pushing -// to an existing bot PR's branch still gets full review of their own commits. -describe("automation-bot-skip: end-to-end webhook + re-entry wiring (#automation-bot-skip)", () => { - const basePayload = { - installation: { id: 9101, account: { login: "owner", id: 1, type: "Organization" } }, - repository: { name: "bot-skip-repo", full_name: "owner/bot-skip-repo", private: false, owner: { login: "owner" } }, - }; - - // resolveRepositorySettings itself probes for a config-as-code override (.gittensory.yml/.json in both the - // repo root and .github/) BEFORE the skip check can even run (it needs the resolved settings for the - // per-repo override) -- so those 4 raw.githubusercontent.com probes are unavoidable, pre-existing overhead - // on EVERY webhook, not the "waste" this feature eliminates. The real signal is that NOTHING beyond that - // touches the actual GitHub REST API (api.github.com) -- no installation-token fetch, no PR/files read, no - // comment/check-run publish, no AI provider call. - async function fetchCallTracker() { - const state = { urls: [] as string[] }; - vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { - state.urls.push(input.toString()); - return new Response("not found", { status: 404 }); - }); - return state; - } - - it("a genuine bot-triggered PR (sender IS the bot, matching the stored author) is skipped entirely: audited, zero GitHub/AI fetch calls, delivery marked processed", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - const calls = await fetchCallTracker(); - - await processJob(env, { - type: "github-webhook", - deliveryId: "bot-skip-genuine", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - sender: { login: "renovate[bot]", type: "Bot" }, - pull_request: { number: 401, title: "chore(deps): bump foo", state: "open", user: { login: "renovate[bot]", type: "Bot" }, labels: [], body: "" }, - }, - }); - - expect(calls.urls.some((url) => url.includes("api.github.com"))).toBe(false); - const skipAudit = await env.DB.prepare("select detail, actor from audit_events where event_type = 'github_app.automation_bot_pr_skipped' and target_key = 'owner/bot-skip-repo#401'").first<{ detail: string; actor: string }>(); - expect(skipAudit?.actor).toBe("renovate[bot]"); - expect(skipAudit?.detail).toContain("automation-bot author"); - const webhookEvent = await env.DB.prepare("select status from webhook_events where delivery_id = 'bot-skip-genuine'").first<{ status: string }>(); - expect(webhookEvent?.status).toBe("processed"); - }); - - it("SECURITY: a human who pushes to an existing bot-authored PR's branch (synchronize) is NOT skipped -- the live webhook actor, not the stored author, gates the skip", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await fetchCallTracker(); - - await processJob(env, { - type: "github-webhook", - deliveryId: "bot-skip-exploit-attempt", - eventName: "pull_request", - payload: { - action: "synchronize", - ...basePayload, - sender: { login: "malicious-contributor", type: "User" }, - pull_request: { number: 402, title: "chore(deps): bump foo", state: "open", user: { login: "renovate[bot]", type: "Bot" }, labels: [], body: "", head: { sha: "hijacked-sha" } }, - }, - }); - - const skipAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.automation_bot_pr_skipped' and target_key = 'owner/bot-skip-repo#402'").first<{ n: number }>(); - expect(skipAudit?.n).toBe(0); - }); - - it("a per-repo 'off' override forces full review even for a genuine bot-triggered PR", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await fetchCallTracker(); - await upsertRepositorySettings(env, { repoFullName: "owner/bot-skip-repo", skipAutomationBotAuthors: "off" }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "bot-skip-repo-off-override", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - sender: { login: "dependabot[bot]", type: "Bot" }, - pull_request: { number: 403, title: "chore(deps): bump bar", state: "open", user: { login: "dependabot[bot]", type: "Bot" }, labels: [], body: "" }, - }, - }); - - const skipAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.automation_bot_pr_skipped' and target_key = 'owner/bot-skip-repo#403'").first<{ n: number }>(); - expect(skipAudit?.n).toBe(0); - }); - - it("a per-repo 'enabled' override skips a genuine bot-triggered PR even when the global default is OFF", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_SKIP_AUTOMATION_BOT_PRS: "false" }); - const calls = await fetchCallTracker(); - await upsertRepositorySettings(env, { repoFullName: "owner/bot-skip-repo", skipAutomationBotAuthors: "enabled" }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "bot-skip-repo-enabled-override", - eventName: "pull_request", - payload: { - action: "opened", - ...basePayload, - sender: { login: "github-actions[bot]", type: "Bot" }, - pull_request: { number: 404, title: "chore(release): 1.2.3", state: "open", user: { login: "github-actions[bot]", type: "Bot" }, labels: [], body: "" }, - }, - }); - - expect(calls.urls.some((url) => url.includes("api.github.com"))).toBe(false); - const skipAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.automation_bot_pr_skipped' and target_key = 'owner/bot-skip-repo#404'").first<{ n: number }>(); - expect(skipAudit?.n).toBe(1); - }); - - it("the re-entry sweep path (agent-regate-pr) also respects the skip for a stored bot author, without even the live resync fetch", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await upsertInstallation(env, { action: "created", installation: { id: 9101, account: { login: "owner", id: 1, type: "Organization" }, target_type: "Organization", repository_selection: "selected", permissions: {}, events: [] } }); - await upsertRepositoryFromGitHub(env, { name: "bot-skip-repo", full_name: "owner/bot-skip-repo", private: false, owner: { login: "owner" } }, 9101); - await upsertPullRequestFromGitHub(env, "owner/bot-skip-repo", { number: 405, title: "chore(deps): bump baz", state: "open", user: { login: "renovate[bot]", type: "Bot" }, head: { sha: "sha405" }, labels: [], body: "" }); - const calls = await fetchCallTracker(); - - await processJob(env, { type: "agent-regate-pr", deliveryId: "bot-skip-sweep", repoFullName: "owner/bot-skip-repo", prNumber: 405, installationId: 9101 }); - - // The re-entry check runs BEFORE even the live-head resync GET, so a genuine bot author skips without any - // GitHub REST API call at all -- not merely without a comment/check-run publish. - expect(calls.urls.some((url) => url.includes("api.github.com"))).toBe(false); - const stored = await getPullRequest(env, "owner/bot-skip-repo", 405); - expect(stored?.headSha).toBe("sha405"); - }); });