Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { resolveRepositorySettings } from "../settings/repository-settings";
import type { LocalBranchAnalysisInput } from "../signals/local-branch";
import {
isEnabled,
runGittensoryAiReview,
type InlineFinding,
} from "../services/ai-review";
Expand Down Expand Up @@ -3385,6 +3386,23 @@ export function buildSecretScanDiff(
* Fully fail-safe: disabled / ineligible author / no head SHA / non-ok AI / any thrown error → no finding
* and no notes.
*/

export async function shouldStartAiReviewForAdvisory(
env: Env,
args: {
settings: RepositorySettings;
advisory: Pick<Awaited<ReturnType<typeof buildPullRequestAdvisory>>, "headSha">;
repoFullName: string;
author: string | null;
confirmedContributor: boolean;
skipAiReview?: boolean | undefined;
},
): Promise<boolean> {
const packAllowsAnyAuthorBlockingReview = args.settings.gatePack === "oss-anti-slop" && args.settings.aiReviewMode === "block";
if (args.skipAiReview || args.settings.aiReviewMode === "off" || (!args.confirmedContributor && !packAllowsAnyAuthorBlockingReview) || !args.advisory.headSha || !isEnabled(env.AI_SUMMARIES_ENABLED) || !isEnabled(env.AI_PUBLIC_COMMENTS_ENABLED) || !env.AI) return false;
return !(isReputationEnabled(env) && isConvergenceRepoAllowed(env, args.repoFullName) && (await shouldSkipAiForReputation(env, { project: args.repoFullName, submitter: args.author })));
}

export async function runAiReviewForAdvisory(
env: Env,
args: {
Expand Down Expand Up @@ -4281,10 +4299,15 @@ async function maybePublishPrPublicSurface(
settings.contributorBlacklist,
);
const aiReviewWillRun =
!webhook.skipAiReview &&
settings.aiReviewMode !== "off" &&
Boolean(advisory.headSha) &&
!authorBlacklisted;
!authorBlacklisted &&
(await shouldStartAiReviewForAdvisory(env, {
settings,
advisory,
repoFullName,
author,
confirmedContributor,
skipAiReview: webhook.skipAiReview,
}));
// Post a transient "🟪 reviewing…" placeholder BEFORE the AI runs so contributors see the bot
// is actively working rather than silent. In-place upsert: once the final verdict is ready it
// overwrites this comment. Best-effort — a failed post never aborts the review. (#reviewing-placeholder)
Expand Down
31 changes: 30 additions & 1 deletion test/unit/ai-review-advisory.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildAiReviewDiff, runAiReviewForAdvisory } from "../../src/queue/processors";
import { buildAiReviewDiff, runAiReviewForAdvisory, shouldStartAiReviewForAdvisory } from "../../src/queue/processors";
import { BEST_REVIEW_MODELS } from "../../src/services/ai-review";
import { upsertRepositoryAiKey } from "../../src/db/repositories";
import type { Advisory, PullRequestFileRecord, RepositorySettings } from "../../src/types";
Expand Down Expand Up @@ -66,6 +66,35 @@ function aiEnv(run: () => Promise<unknown>, flags = true) {
});
}

describe("shouldStartAiReviewForAdvisory", () => {
const enabledEnv = () => aiEnv(async () => ({ response: notesOnlyJson() }));
const base = { settings: { aiReviewMode: "advisory", gatePack: "gittensor" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", author: "alice", confirmedContributor: true };

it("matches the AI review entry gates before the reviewing placeholder is posted", async () => {
await expect(shouldStartAiReviewForAdvisory(enabledEnv(), base)).resolves.toBe(true);
await expect(shouldStartAiReviewForAdvisory(enabledEnv(), { ...base, skipAiReview: true })).resolves.toBe(false);
await expect(shouldStartAiReviewForAdvisory(enabledEnv(), { ...base, settings: { aiReviewMode: "off" } as RepositorySettings })).resolves.toBe(false);
await expect(shouldStartAiReviewForAdvisory(enabledEnv(), { ...base, confirmedContributor: false })).resolves.toBe(false);
await expect(shouldStartAiReviewForAdvisory(enabledEnv(), { ...base, settings: { aiReviewMode: "block", gatePack: "oss-anti-slop" } as RepositorySettings, confirmedContributor: false })).resolves.toBe(true);
const noSha = advisory();
delete (noSha as Partial<Advisory>).headSha;
await expect(shouldStartAiReviewForAdvisory(enabledEnv(), { ...base, advisory: noSha })).resolves.toBe(false);
});

it("does not start when AI comments are disabled or the Workers AI binding is unavailable", async () => {
const commentsDisabled = createTestEnv({ AI: { run: vi.fn() } as unknown as Ai, AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "false" });
await expect(shouldStartAiReviewForAdvisory(commentsDisabled, base)).resolves.toBe(false);
const missingBinding = createTestEnv({ AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" });
await expect(shouldStartAiReviewForAdvisory(missingBinding, base)).resolves.toBe(false);
});

it("does not start when the reputation gate downgrades the PR to deterministic-only", async () => {
const env = createTestEnv({ AI: { run: vi.fn() } as unknown as Ai, AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true", GITTENSORY_REVIEW_REPUTATION: "true", GITTENSORY_REVIEW_REPOS: "acme/widgets" });
await env.DB.prepare("INSERT INTO submitter_stats (project, submitter, submissions, merged, closed, manual, last_seen) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)").bind("acme/widgets", "alice", 8, 0, 8, 0).run();
await expect(shouldStartAiReviewForAdvisory(env, base)).resolves.toBe(false);
});
});

describe("runAiReviewForAdvisory", () => {
it("no-ops when aiReviewMode is off", async () => {
const adv = advisory();
Expand Down
52 changes: 51 additions & 1 deletion test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,8 @@ describe("queue processors", () => {
autoLabelEnabled: false,
checkRunMode: "off",
gateCheckMode: "enabled",
aiReviewMode: "advisory",
aiReviewMode: "block",
gatePack: "oss-anti-slop",
});
const commentBodies: string[] = [];
let firstCommentWasPlaceholder = false;
Expand Down Expand Up @@ -823,6 +824,55 @@ describe("queue processors", () => {
expect(commentBodies.some((body) => !body.includes("is reviewing"))).toBe(true);
});

it("does not post the 🟪 reviewing placeholder when public AI comments are disabled (regression)", 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", aiReviewMode: "advisory" });
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/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") {
commentBodies.push(String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).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: "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(commentBodies.length).toBe(1);
expect(commentBodies[0]).not.toContain("is reviewing");
expect(commentBodies[0]).not.toContain("🟪");
});

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: [] } });
Expand Down
Loading