From 46702ce876bd62dcbead1ed0336f39e06f48f7d6 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:52:14 -0700 Subject: [PATCH] fix(commands): resolve duplicate @gittensory review handler breaking typecheck Two independently-merged PRs (#4050 and #4175) both implemented the @gittensory review command, leaving two colliding declarations of maybeProcessReviewCommand/recordReviewCommandSkip and two dispatch call sites -- a hard tsc "Duplicate function implementation" error that blocks every PR's typecheck/validate check on main. Keeps the earlier, more complete implementation (which also wires the paired resume command and a real hasAutoreviewPausedMarker fix), and folds in two things only the later, now-removed duplicate got right: - needsMinerDetection: true on the authorization call. "review" is deliberately widened to confirmed_miner (self-rerun precedent, same as review-now), so without this flag a confirmed miner re-triggering review on their own PR had no other role to match and was always denied. - A resolveAgentActionMode pause/dry-run gate before dispatching, matching every other action command (pause/resolve/explain/ gate-override/generate-tests). The kept implementation had no such gate at all, so review always dispatched live regardless of a maintainer's global pause or dry-run setting. Removes the redundant duplicate test suite for the same command and adds regression coverage for both fixes above. --- src/queue/processors.ts | 200 +-------------- test/unit/queue.test.ts | 535 ++++------------------------------------ 2 files changed, 66 insertions(+), 669 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index e3942b53f2..4fac0fb7b6 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -5654,22 +5654,6 @@ async function processGitHubWebhook( return; } - if ( - eventName === "issue_comment" && - (await maybeProcessReviewCommand(env, deliveryId, payload)) - ) { - await recordWebhookEvent(env, { - deliveryId, - eventName, - action: payload.action, - installationId: payload.installation?.id, - repositoryFullName: payload.repository?.full_name, - payloadHash: "processed", - status: "processed", - }); - return; - } - if ( eventName === "issue_comment" && (await maybeProcessGateOverrideCommand(env, deliveryId, payload)) @@ -10892,177 +10876,6 @@ export async function resolveOverrideHeadSha( * payload.comment.author_association. The override is intentionally NOT persisted: a follow-up push * re-evaluates the Gate from scratch (no permanent bypass). */ -/** - * `@gittensory review` / `re-review` (#2163): dispatch a manual auto-review re-run via the existing - * `reReviewStoredPullRequest` path. Affects only auto-review scheduling/output — never gate disposition - * (#1960). Honors the shared PR-command classifier, real repo permission auth, and pause/dry-run like - * gate-override/resolve. - */ -async function maybeProcessReviewCommand( - env: Env, - deliveryId: string, - payload: GitHubWebhookPayload, -): Promise { - const command = parseGittensoryMentionCommand(payload.comment?.body); - if (!command || command.name !== "review") return false; - - const req = classifyPrCommandRequest(payload, getInstallationId(payload)); - if (!req.ok) { - await recordReviewCommandSkip( - env, - deliveryId, - req.repoFullName, - req.targetKey, - req.actor, - req.reason, - ); - return true; - } - - const [pr, settings] = await Promise.all([ - getPullRequest(env, req.repoFullName, req.pr.number), - resolveRepositorySettings(env, req.repoFullName), - ]); - const targetKey = `${req.repoFullName}#${req.pr.number}`; - if (!pr) { - await recordReviewCommandSkip( - env, - deliveryId, - req.repoFullName, - targetKey, - req.actor, - "cached_pr_missing", - ); - return true; - } - - const { authorization } = await authorizePrActionActor({ - env, - deliveryId, - installationId: req.installationId, - repoFullName: req.repoFullName, - issue: payload.issue!, - actor: req.actor, - commandName: "review" as GittensoryMentionCommandName, - settings, - pr, - needsMinerDetection: true, - }); - if (!authorization.authorized) { - await recordAuditEvent(env, { - eventType: "github_app.review_command_denied", - actor: req.actor, - targetKey, - outcome: "denied", - detail: authorization.reason, - metadata: { - deliveryId, - repoFullName: req.repoFullName, - allowedRoles: commandAuthorizationAllowedRoles( - settings.commandAuthorization, - "review", - ), - }, - }); - await recordGithubProductUsage(env, "review_command_denied", { - actor: req.actor, - repoFullName: req.repoFullName, - targetKey, - outcome: "denied", - metadata: { - reason: authorization.reason, - actorKind: authorization.actorKind, - allowedRoles: commandAuthorizationAllowedRoles( - settings.commandAuthorization, - "review", - ), - }, - }); - return true; - } - - const mode = resolveAgentActionMode({ - globalPaused: isGlobalAgentPause(env) || (await isGlobalAgentFrozen(env)), - agentPaused: settings.agentPaused, - agentDryRun: settings.agentDryRun, - }); - if (mode !== "live") { - const skipReason = mode === "dry_run" ? "dry_run" : "agent_paused"; - await recordReviewCommandSkip( - env, - deliveryId, - req.repoFullName, - targetKey, - req.actor, - skipReason, - ); - return true; - } - - await reReviewStoredPullRequest( - env, - deliveryId, - req.installationId, - req.repoFullName, - req.pr.number, - undefined, - { - skipAiReview: settings.aiReviewMode === "off", - force: true, - }, - ); - - await recordAuditEvent(env, { - eventType: "github_app.review_command_completed", - actor: req.actor, - targetKey, - outcome: "completed", - metadata: { - deliveryId, - repoFullName: req.repoFullName, - headSha: pr.headSha ?? null, - commentId: payload.comment?.id ?? null, - }, - }); - await recordGithubProductUsage(env, "review_command_completed", { - actor: req.actor, - repoFullName: req.repoFullName, - targetKey, - outcome: "completed", - metadata: { - actorKind: authorization.actorKind, - headSha: pr.headSha ?? null, - commentId: payload.comment?.id ?? null, - }, - }); - return true; -} - -async function recordReviewCommandSkip( - env: Env, - deliveryId: string, - repoFullName: string | null | undefined, - targetKey: string | null | undefined, - actor: string | null, - reason: string, -): Promise { - await recordAuditEvent(env, { - eventType: "github_app.review_command_skipped", - actor, - targetKey, - outcome: "completed", - detail: reason, - metadata: { deliveryId, repoFullName: repoFullName ?? null, reason }, - }); - await recordGithubProductUsage(env, "review_command_skipped", { - actor, - repoFullName, - targetKey, - outcome: "skipped", - metadata: { reason }, - }); -} - async function maybeProcessGateOverrideCommand( env: Env, deliveryId: string, @@ -11384,12 +11197,23 @@ async function maybeProcessReviewCommand(env: Env, deliveryId: string, payload: await recordReviewCommandSkip(env, deliveryId, req.repoFullName, targetKey, req.actor, "cached_pr_missing"); return true; } - const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "review" as GittensoryMentionCommandName, settings, pr }); + // needsMinerDetection: true -- "review" is deliberately widened to confirmed_miner (see the doc comment + // above and DEFAULT_COMMAND_AUTHORIZATION_POLICY's own comment on this command), so the miner-status lookup + // authorizePrActionActor gates behind this flag MUST run here, or a confirmed miner re-triggering review on + // their own PR is wrongly denied (there is no other role they could match instead). + const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "review" as GittensoryMentionCommandName, settings, pr, needsMinerDetection: true }); if (!authorization.authorized) { await recordAuditEvent(env, { eventType: "github_app.review_command_denied", actor: req.actor, targetKey, outcome: "denied", detail: authorization.reason, metadata: { deliveryId, repoFullName: req.repoFullName, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "review") } }); await recordGithubProductUsage(env, "review_command_denied", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "denied", metadata: { reason: authorization.reason, actorKind: authorization.actorKind, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "review") } }); return true; } + // Same dry-run/paused gate every other action command respects (pause/resolve/explain/gate-override/ + // generate-tests) -- a paused or dry-run repo must not dispatch a live re-review or post a confirmation. + const mode = resolveAgentActionMode({ globalPaused: isGlobalAgentPause(env) || (await isGlobalAgentFrozen(env)), agentPaused: settings.agentPaused, agentDryRun: settings.agentDryRun }); + if (mode !== "live") { + await recordReviewCommandSkip(env, deliveryId, req.repoFullName, targetKey, req.actor, mode === "dry_run" ? "dry_run" : "agent_paused"); + return true; + } const confirmation = sanitizePublicComment([AGENT_COMMAND_COMMENT_MARKER, "", "> [!NOTE]", `> **Re-review triggered by @${req.actor}**`, "> Re-running auto-review for this PR. The Gate check-run and one-shot disposition are produced the same way a scheduled pass would.", "", "---", gittensoryFooter()].join("\n")); await createIssueComment(env, req.installationId, req.repoFullName, req.pr.number, confirmation); await reReviewStoredPullRequest(env, deliveryId, req.installationId, req.repoFullName, req.pr.number, undefined, { force: true }); diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 9b69174e03..bf05e31bfb 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -11237,6 +11237,60 @@ describe("queue processors", () => { 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(); + }); + + 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 @@ -24036,487 +24090,6 @@ describe("queue processors", () => { expect(overridden ?? null).toBeNull(); }); - // #2163: `@gittensory review` / `re-review` dispatches to reReviewStoredPullRequest without mutating gate disposition. - describe("@gittensory review (#2163)", () => { - async function seedReviewCommandPr( - env: Env, - overrides: Partial[1]> = {}, - ): 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" } }], - }); - await upsertRepositorySettings(env, { - repoFullName: "JSONbored/gittensory", - commentMode: "off", - publicSurface: "off", - autoLabelEnabled: false, - checkRunMode: "off", - gateCheckMode: "enabled", - linkedIssueGateMode: "off", - aiReviewMode: "off", - ...overrides, - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 2163, - title: "Review me", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: { sha: "review-2163-sha" }, - labels: [], - body: "Validation: npm test", - }); - await upsertPullRequestDetailSyncState(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 2163, - status: "complete", - reviewsSyncedAt: new Date().toISOString(), - }); - } - - function reviewCommandWebhook( - commentBody: string, - actor: string, - overrides: { action?: string; issue?: Record; comment?: Record } = {}, - ): Parameters[1] { - return { - type: "github-webhook", - deliveryId: `review-2163-${actor}-${commentBody.length}`, - eventName: "issue_comment", - payload: { - action: overrides.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: overrides.issue ?? { - number: 2163, - title: "Review me", - state: "open", - user: { login: "contributor" }, - pull_request: {}, - }, - comment: { - id: 21630, - body: commentBody, - author_association: "NONE", - user: { login: actor, type: "User" }, - ...(overrides.comment ?? {}), - }, - sender: { login: actor, type: "User" }, - }, - } as unknown as Parameters[1]; - } - - it("dispatches reReviewStoredPullRequest for an authorized maintainer and leaves gate mode untouched", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewCommandPr(env); - const calls = { permission: 0, livePullGets: 0, gatePatches: 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")) { - calls.permission += 1; - return Response.json({ permission: "admin" }); - } - if (url.endsWith("/pulls/2163")) { - calls.livePullGets += 1; - return Response.json({ - number: 2163, - title: "Review me", - state: "open", - draft: false, - user: { login: "contributor" }, - head: { sha: "review-2163-sha" }, - labels: [], - body: "Validation: npm test", - mergeable_state: "clean", - }); - } - if (url.includes("/commits/review-2163-sha/check-runs") && method === "GET") { - return Response.json({ total_count: 0, check_runs: [] }); - } - if (url.includes("/commits/review-2163-sha/status")) { - return Response.json({ state: "success", statuses: [] }); - } - if (url.includes("/pulls/2163/files")) { - return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+ok" }]); - } - if (url.includes("/issues/2163/comments")) { - return method === "POST" ? Response.json({ id: 21631 }, { status: 201 }) : Response.json([]); - } - if (url.includes("/check-runs") && method === "PATCH") { - calls.gatePatches += 1; - return Response.json({ id: 1 }); - } - if (url.includes("/branches/")) { - return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, reviewCommandWebhook("@gittensory review", "maintainer")); - - expect(calls.permission).toBe(1); - expect(calls.livePullGets).toBeGreaterThan(0); - const completed = await env.DB.prepare("select event_type, actor, target_key, outcome from audit_events where event_type = ?") - .bind("github_app.review_command_completed") - .first<{ event_type: string; actor: string; target_key: string; outcome: string }>(); - expect(completed).toMatchObject({ - event_type: "github_app.review_command_completed", - actor: "maintainer", - target_key: "JSONbored/gittensory#2163", - outcome: "completed", - }); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ surface: "github_app", eventName: "review_command_completed", outcome: "completed" }), - ]), - ); - 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 gateOverride = await env.DB.prepare("select id from audit_events where event_type = ?") - .bind("github_app.gate_overridden") - .first<{ id: string }>(); - expect(gateOverride ?? null).toBeNull(); - }); - - it("treats @gittensory re-review as the review command alias", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewCommandPr(env); - 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/2163")) { - return Response.json({ - number: 2163, - title: "Review me", - state: "open", - draft: false, - user: { login: "contributor" }, - head: { sha: "review-2163-sha" }, - labels: [], - body: "Validation: npm test", - mergeable_state: "clean", - }); - } - if (url.includes("/commits/review-2163-sha/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); - if (url.includes("/commits/review-2163-sha/status")) return Response.json({ state: "success", statuses: [] }); - if (url.includes("/pulls/2163/files")) { - return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+ok" }]); - } - if (url.includes("/issues/2163/comments")) { - return method === "POST" ? Response.json({ id: 21631 }, { status: 201 }) : Response.json([]); - } - if (url.includes("/branches/")) { - return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, reviewCommandWebhook("@gittensory re-review", "maintainer")); - - 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"); - }); - - it("denies an unauthorized actor with review_command_denied audit + usage", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewCommandPr(env); - const calls = { livePullGets: 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("/collaborators/outsider/permission")) return Response.json({ permission: "read" }); - if (url.endsWith("/pulls/2163")) { - calls.livePullGets += 1; - return Response.json({ number: 2163, state: "open", head: { sha: "review-2163-sha" } }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, reviewCommandWebhook("@gittensory review", "outsider")); - - expect(calls.livePullGets).toBe(0); - const denied = await env.DB.prepare("select event_type, actor, outcome, detail from audit_events where event_type = ?") - .bind("github_app.review_command_denied") - .first<{ event_type: string; actor: string; outcome: string; detail: string }>(); - expect(denied).toMatchObject({ - event_type: "github_app.review_command_denied", - actor: "outsider", - outcome: "denied", - detail: "not_maintainer_or_pr_author", - }); - const usageEvents = await listProductUsageEvents(env, { limit: 10 }); - expect(usageEvents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ eventName: "review_command_denied", outcome: "denied" }), - ]), - ); - const completed = await env.DB.prepare("select id from audit_events where event_type = ?") - .bind("github_app.review_command_completed") - .first<{ id: string }>(); - expect(completed ?? null).toBeNull(); - }); - - it("records classifier skips for bot authors, edited comments, and missing PR targets", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewCommandPr(env); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); - - await processJob( - env, - reviewCommandWebhook("@gittensory review", "some-bot[bot]", { - comment: { user: { login: "some-bot[bot]", type: "Bot" } }, - }), - ); - 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("bot_author"); - - await processJob(env, reviewCommandWebhook("@gittensory review", "maintainer", { action: "edited" })); - 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("unsupported_comment_action"); - - await processJob( - env, - reviewCommandWebhook("@gittensory review", "maintainer", { - issue: { number: 2163, title: "Not a PR", state: "open", user: { login: "reporter" } }, - }), - ); - 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("missing_repo_pr_installation_or_actor"); - }); - - it("skips when the cached PR row is missing", 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: ["issues", "issue_comment"], - }, - 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", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - 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, reviewCommandWebhook("@gittensory review", "maintainer")); - - const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") - .bind("github_app.review_command_skipped") - .first<{ detail: string }>(); - expect(skipped?.detail).toBe("cached_pr_missing"); - }); - - it("respects agentPaused and agentDryRun without dispatching re-review", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewCommandPr(env, { agentPaused: true }); - const calls = { livePullGets: 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("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.endsWith("/pulls/2163")) { - calls.livePullGets += 1; - return Response.json({ number: 2163, state: "open", head: { sha: "review-2163-sha" } }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, reviewCommandWebhook("@gittensory review", "maintainer")); - expect(calls.livePullGets).toBe(0); - 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"); - - await seedReviewCommandPr(env, { agentPaused: false, agentDryRun: true }); - await processJob(env, reviewCommandWebhook("@gittensory review", "maintainer")); - 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"); - }); - - it("records completed metadata fallbacks when head sha and comment id are absent", 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: ["issues", "issue_comment"], - }, - 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", - linkedIssueGateMode: "off", - aiReviewMode: "off", - }); - await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { - number: 2164, - title: "Review me (no head)", - state: "open", - user: { login: "contributor" }, - author_association: "CONTRIBUTOR", - head: {}, - labels: [], - body: "Validation: npm test", - }); - await upsertPullRequestDetailSyncState(env, { - repoFullName: "JSONbored/gittensory", - pullNumber: 2164, - status: "complete", - reviewsSyncedAt: new Date().toISOString(), - }); - 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/2164")) { - return Response.json({ - number: 2164, - title: "Review me (no head)", - state: "open", - draft: false, - user: { login: "contributor" }, - head: {}, - labels: [], - body: "Validation: npm test", - mergeable_state: "clean", - }); - } - if (url.includes("/pulls/2164/files")) { - return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+ok" }]); - } - if (url.includes("/issues/2164/comments")) { - return method === "POST" ? Response.json({ id: 21641 }, { status: 201 }) : Response.json([]); - } - if (url.includes("/branches/")) { - return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); - } - return new Response("not found", { status: 404 }); - }); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-2163-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: 2164, title: "Review me (no head)", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { body: "@gittensory review", author_association: "NONE", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - } as unknown as Parameters[1]); - - const completed = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") - .bind("github_app.review_command_completed") - .first<{ metadata_json: string }>(); - expect(JSON.parse(completed?.metadata_json ?? "{}")).toMatchObject({ - headSha: null, - commentId: null, - }); - }); - - it("records skip metadata when the classifier has no repository context", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); - - await processJob(env, { - type: "github-webhook", - deliveryId: "review-2163-no-repo", - eventName: "issue_comment", - payload: { - action: "created", - installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, - issue: { number: 2163, title: "Review me", state: "open", user: { login: "contributor" }, pull_request: {} }, - comment: { body: "@gittensory review", user: { login: "maintainer", type: "User" } }, - sender: { login: "maintainer", type: "User" }, - }, - } as unknown as Parameters[1]); - - const skipped = await env.DB.prepare("select metadata_json, detail from audit_events where event_type = ?") - .bind("github_app.review_command_skipped") - .first<{ metadata_json: string; detail: string }>(); - expect(skipped?.detail).toBe("missing_repo_pr_installation_or_actor"); - expect(JSON.parse(skipped?.metadata_json ?? "{}")).toMatchObject({ repoFullName: null }); - }); - - it("does not intercept non-review comments", async () => { - const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); - await seedReviewCommandPr(env); - vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); - - await processJob(env, reviewCommandWebhook("just chatting, no command", "maintainer")); - - const events = await env.DB.prepare("select event_type from audit_events where event_type like ?") - .bind("github_app.review_command%") - .all<{ event_type: string }>(); - expect(events.results ?? []).toHaveLength(0); - }); - }); - // #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) {