From 3f4b18d9aaeba16ba8dff2e2d7196727511702ec Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Thu, 9 Jul 2026 05:32:09 -0700 Subject: [PATCH] fix(review): gate every AI-spend call site on the repo's paused/frozen state resolveRepoActionMode() is already computed once per pass in maybePublishPrPublicSurface and correctly threaded into every GitHub-write call (labels, comments, check-runs) -- but never into the four functions that actually spend real tokens (runAiReviewForAdvisory, runAiSlopForAdvisory, runLinkedIssueSatisfactionForAdvisory, runVisualVisionForAdvisory) or the maintainer-invoked agent-run summary (attachPrivateAiSummary). Each of those was gated only by its own independent feature flag (aiReviewMode, slop.aiAdvisory, linkedIssueSatisfactionGateMode), completely orthogonal to the fleet-wide env brake, the DB freeze, and per-repo pause. This meant reactivating a repo with a large stale PR backlog (or simply forgetting one of several independent settings while trying to pause) let already-queued and newly-triggered review jobs keep spending for as long as the backlog took to drain -- confirmed live: two repos burned through 500 fresh AI-review/slop calls across 98 distinct PRs over several hours before the queue emptied, because re-engaging the freeze override alone did not stop already-dispatched work from reaching the LLM call once dequeued. Threads the mode already resolved by the caller into all five spend paths, with "paused" as the first check in each -- gating at the point of spend rather than only at the point of dispatch, so a freeze that engages between enqueue and execution is still honored. "dry_run" still computes (so a maintainer can validate review decisions locally); only "paused" stops spend outright. --- src/queue/processors.ts | 28 ++++++++- src/services/agent-orchestrator.ts | 11 +++- .../advisory-ai-routing-call-sites.test.ts | 3 + test/unit/agent-orchestrator.test.ts | 12 ++++ test/unit/ai-review-advisory.test.ts | 54 ++++++++++++++-- test/unit/ai-slop.test.ts | 37 +++++++---- test/unit/enrichment-wiring.test.ts | 4 ++ test/unit/grounding-wiring.test.ts | 2 + test/unit/impact-map-processor-wiring.test.ts | 4 ++ .../linked-issue-satisfaction-run.test.ts | 61 +++++++++++-------- test/unit/rag-wiring.test.ts | 1 + test/unit/repo-culture-profile-wiring.test.ts | 3 + .../repository-settings-enforcement.test.ts | 1 + test/unit/reputation-wiring.test.ts | 2 +- test/unit/visual-vision-wiring.test.ts | 40 ++++++++++++ 15 files changed, 217 insertions(+), 46 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index d9c3d04aae..ac511bb70f 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -259,6 +259,7 @@ import { isGlobalAgentPause, resolveAgentActionMode, resolveAgentPermissionReadiness, + type AgentActionMode, } from "../settings/agent-execution"; import { ISSUE_WAKE_MAX_PRS, @@ -6990,6 +6991,13 @@ async function resolveReviewEnrichmentGithubToken( export async function runAiReviewForAdvisory( env: Env, args: { + // The caller's already-resolved resolveRepoActionMode() result (#token-bleed-spend-gate): a "paused" repo + // must NEVER reach the LLM call below, full stop -- not just have its GitHub publish suppressed. Every + // feature-specific gate below (aiReviewMode, confirmedContributor, ...) is independent of this and was, on + // its own, insufficient: a fleet-wide freeze or per-repo pause with aiReviewMode still "block"/"advisory" + // spent real tokens for hours on frozen repos before this field existed. "dry_run" still computes (so a + // maintainer can validate decision logic locally); only "paused" stops spend. + mode: AgentActionMode; settings: RepositorySettings; advisory: Awaited>; installationId?: number | null | undefined; @@ -7101,6 +7109,7 @@ export async function runAiReviewForAdvisory( packAllowsAnyAuthorBlockingReview || args.settings.aiReviewAllAuthors; if ( + args.mode === "paused" || args.settings.aiReviewMode === "off" || !reviewableAuthor || !args.advisory.headSha @@ -7675,6 +7684,9 @@ export async function maybeAddLockfileTamperFinding( export async function runAiSlopForAdvisory( env: Env, args: { + // See runAiReviewForAdvisory's doc comment on this same field (#token-bleed-spend-gate) -- a paused repo + // must never reach the LLM call below, independent of settings.slopAiAdvisory. + mode: AgentActionMode; settings: RepositorySettings; advisory: Awaited>; repoFullName: string; @@ -7687,7 +7699,7 @@ export async function runAiSlopForAdvisory( ): Promise { // Confirmed-contributor gate (matches runAiReviewForAdvisory): no AI spend — free OR BYOK — on a PR from // an unconfirmed author. The deterministic slop core still ran for everyone; only the AI layer is gated. - if (!args.confirmedContributor || !args.advisory.headSha) return; + if (args.mode === "paused" || !args.confirmedContributor || !args.advisory.headSha) return; try { // BYOK (opt-in): reuse the repo's encrypted key + aiReviewByok flag — one BYOK key serves both AI // features. A declared provider must match the stored key's provider, else skip BYOK (Workers-AI @@ -7817,6 +7829,9 @@ export async function runAiSlopForAdvisory( export async function runLinkedIssueSatisfactionForAdvisory( env: Env, args: { + // See runAiReviewForAdvisory's doc comment on this same field (#token-bleed-spend-gate) -- a paused repo + // must never reach the LLM call below, independent of settings.linkedIssueSatisfactionGateMode. + mode: AgentActionMode; settings: RepositorySettings; advisory: Awaited>; repoFullName: string; @@ -7827,7 +7842,7 @@ export async function runLinkedIssueSatisfactionForAdvisory( installationId: number; }, ): Promise<{ status: "addressed" | "partial" | "unaddressed"; rationale: string } | null> { - if (!args.confirmedContributor || !args.advisory.headSha) return null; + if (args.mode === "paused" || !args.confirmedContributor || !args.advisory.headSha) return null; const primaryIssueNumber = args.pr.linkedIssues[0]; if (primaryIssueNumber === undefined) return null; try { @@ -8235,6 +8250,9 @@ async function runSelfHostVisualVision(env: Env, system: string, user: string, i export async function runVisualVisionForAdvisory( env: Env, args: { + // See runAiReviewForAdvisory's doc comment on this same field (#token-bleed-spend-gate) -- a paused repo + // must never reach the vision-model call below. + mode: AgentActionMode; repoFullName: string; pr: { number: number }; author: string | null; @@ -8244,7 +8262,7 @@ export async function runVisualVisionForAdvisory( routes: readonly CaptureRoute[]; }, ): Promise { - if (args.routes.length === 0) return; + if (args.mode === "paused" || args.routes.length === 0) return; try { const visionReputation = await getSubmitterReputation(env, args.repoFullName, args.author ?? undefined); // BYOK resolution mirrors runAiReviewForAdvisory's own (re-resolved per-caller is this codebase's @@ -9152,6 +9170,7 @@ async function maybePublishPrPublicSurface( // advisory-only finding. Deliberately does NOT update slopRisk — only the deterministic core blocks. if (shouldRunSlopAiAdvisory(settings)) { await runAiSlopForAdvisory(env, { + mode, settings, advisory, repoFullName, @@ -9170,6 +9189,7 @@ async function maybePublishPrPublicSurface( // to function scope above, alongside gateEvaluation, since it is consumed later outside this try block.) if (settings.linkedIssueSatisfactionGateMode !== "off" && pr.linkedIssues.length > 0) { linkedIssueSatisfaction = await runLinkedIssueSatisfactionForAdvisory(env, { + mode, settings, advisory, repoFullName, @@ -9826,6 +9846,7 @@ async function maybePublishPrPublicSurface( }).catch(() => undefined); } aiReview = await runAiReviewForAdvisory(env, { + mode, settings, advisory, installationId, @@ -10727,6 +10748,7 @@ async function maybePublishPrPublicSurface( // own doc comment. Deliberately independent of the capture block above (its own try/catch there) so a // vision failure can never affect the "Visual preview" section that block already rendered. await runVisualVisionForAdvisory(env, { + mode, repoFullName, pr, author, diff --git a/src/services/agent-orchestrator.ts b/src/services/agent-orchestrator.ts index 69a8cf05aa..6b0f603bf7 100644 --- a/src/services/agent-orchestrator.ts +++ b/src/services/agent-orchestrator.ts @@ -30,6 +30,8 @@ import { buildContributorOpenPrMonitor, type ContributorOpenPrMonitor } from ".. import { buildLocalBranchAnalysis, findCurrentBranchPullRequest, type LocalBranchAnalysis, type LocalBranchAnalysisInput } from "../signals/local-branch"; import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; import { resolveRepositorySettings } from "../settings/repository-settings"; +import { resolveRepoActionMode } from "../github/client"; +import { isGlobalAgentPause } from "../settings/agent-execution"; import { withAdvisoryAiEnv } from "../selfhost/ai"; import { withAgentActionExplanationCard } from "./agent-action-explanation-card"; import { attachRecommendationSnapshots } from "./recommendation-snapshots"; @@ -229,7 +231,14 @@ async function attachPrivateAiSummary(env: Env, bundle: AgentRunBundle): Promise // slop/e2e-test-gen/planner. repoFullName can be absent for a cross-repo run (e.g. plan_next_work) -- // falls back to the plain env (byte-identical) rather than resolving settings for an empty key. const repoFullName = String(bundle.run.payload.repoFullName ?? ""); - const routeThroughAdvisory = repoFullName ? (await resolveRepositorySettings(env, repoFullName)).advisoryAiRouting?.summaries === true : false; + const repoSettings = repoFullName ? await resolveRepositorySettings(env, repoFullName) : null; + const routeThroughAdvisory = repoSettings?.advisoryAiRouting?.summaries === true; + // #token-bleed-spend-gate: a paused repo (or the fleet-wide env brake, which applies with no repoFullName at + // all) must never reach the LLM call below -- same reasoning as runAiReviewForAdvisory/runAiSlopForAdvisory in + // src/queue/processors.ts. A cross-repo run (no repoFullName) has no per-repo freeze to check, so only the + // fleet-wide brake applies to it. + const mode = repoSettings ? await resolveRepoActionMode(env, repoSettings) : (isGlobalAgentPause(env) ? "paused" : "live"); + if (mode === "paused") return bundle; const summary = await summarizeAgentBundleWithAi(withAdvisoryAiEnv(env, routeThroughAdvisory), bundle, "private"); if (summary.status === "disabled" || summary.status === "unavailable") return bundle; await updateAgentRun(env, bundle.run.id, { diff --git a/test/unit/advisory-ai-routing-call-sites.test.ts b/test/unit/advisory-ai-routing-call-sites.test.ts index 815e9162cd..7fd76ce548 100644 --- a/test/unit/advisory-ai-routing-call-sites.test.ts +++ b/test/unit/advisory-ai-routing-call-sites.test.ts @@ -34,6 +34,7 @@ describe("runAiSlopForAdvisory routes through AI_ADVISORY (#4364)", () => { AI_ADVISORY: { run: advisoryRun } as unknown as Ai, }); await runAiSlopForAdvisory(env, { + mode: "live", settings: settingsFixture({ slop: true, e2eTestGen: false, planner: false, summaries: false }), advisory, repoFullName: "owner/repo", @@ -57,6 +58,7 @@ describe("runAiSlopForAdvisory routes through AI_ADVISORY (#4364)", () => { AI_ADVISORY: { run: advisoryRun } as unknown as Ai, }); await runAiSlopForAdvisory(env, { + mode: "live", settings: settingsFixture(undefined), advisory, repoFullName: "owner/repo", @@ -75,6 +77,7 @@ describe("runAiSlopForAdvisory routes through AI_ADVISORY (#4364)", () => { advisoryRun.mockClear(); const env = createTestEnv({ AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true", AI: { run: frontierRun } as unknown as Ai }); await runAiSlopForAdvisory(env, { + mode: "live", settings: settingsFixture({ slop: true, e2eTestGen: false, planner: false, summaries: false }), advisory, repoFullName: "owner/repo", diff --git a/test/unit/agent-orchestrator.test.ts b/test/unit/agent-orchestrator.test.ts index 04daace14d..9f7ab7e53a 100644 --- a/test/unit/agent-orchestrator.test.ts +++ b/test/unit/agent-orchestrator.test.ts @@ -12,6 +12,7 @@ import { type AgentRunBundle, } from "../../src/services/agent-orchestrator"; import { buildAgentActionExplanationCard } from "../../src/services/agent-action-explanation-card"; +import * as aiSummariesModule from "../../src/services/ai-summaries"; import { CONTRIBUTOR_DECISION_PACK_SIGNAL, type ContributorDecisionPack, type RepoOutcomeSummary } from "../../src/services/decision-pack"; import { buildPublicAgentCommandComment, parseGittensoryMentionCommand } from "../../src/github/commands"; import { normalizeRegistryPayload } from "../../src/registry/normalize"; @@ -150,6 +151,17 @@ describe("agent orchestrator", () => { }); }); + it("REGRESSION (#token-bleed-spend-gate): the fleet-wide env pause skips the private AI summary spend entirely", async () => { + const summarizeSpy = vi.spyOn(aiSummariesModule, "summarizeAgentBundleWithAi"); + const env = createTestEnv({ AGENT_ACTIONS_PAUSED: "true" }); + await persistDecisionPack(env, decisionPackFixture()); + + await planNextWork(env, { login: "oktofeesh1", repoFullName: "we-promise/sure", objective: "Pick one action" }); + + expect(summarizeSpy).not.toHaveBeenCalled(); + summarizeSpy.mockRestore(); + }); + it("threads scoped counterfactual reasons into decision context snapshots", () => { const generatedAt = nowIso(); const rejectedAlternative = { diff --git a/test/unit/ai-review-advisory.test.ts b/test/unit/ai-review-advisory.test.ts index 09f5b450aa..70df48b0ea 100644 --- a/test/unit/ai-review-advisory.test.ts +++ b/test/unit/ai-review-advisory.test.ts @@ -122,6 +122,7 @@ describe("runAiReviewForAdvisory", () => { it("no-ops when aiReviewMode is off", async () => { const adv = advisory(); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: defectJson() })), { + mode: "live", settings: { aiReviewMode: "off" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -133,6 +134,23 @@ describe("runAiReviewForAdvisory", () => { expect(adv.findings).toEqual([]); }); + it("REGRESSION (#token-bleed-spend-gate): a paused mode never reaches the LLM call, even with aiReviewMode block/advisory and a confirmed contributor", async () => { + const run = vi.fn(async () => ({ response: defectJson() })); + const adv = advisory(); + const result = await runAiReviewForAdvisory(aiEnv(run), { + mode: "paused", + settings: { aiReviewMode: "block" } as RepositorySettings, + advisory: adv, + repoFullName: "acme/widgets", + pr, + author: "alice", + confirmedContributor: true, + }); + expect(result).toBeUndefined(); + expect(adv.findings).toEqual([]); + expect(run).not.toHaveBeenCalled(); + }); + it("survives a focus-manifest load failure during feature resolution (fail-safe → allowlist default, review still runs)", async () => { // loadRepoFocusManifest REJECTS (localManifestReader throws, outside its try/catch) while RAG is flag-enabled, // so runAiReviewForAdvisory takes the featureManifest-load arm and its `.catch(() => null)` fires; reputation/rag @@ -144,6 +162,7 @@ describe("runAiReviewForAdvisory", () => { const env = aiEnv(async () => ({ response: defectJson() })); (env as unknown as { GITTENSORY_REVIEW_RAG: string }).GITTENSORY_REVIEW_RAG = "true"; const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", @@ -164,7 +183,7 @@ describe("runAiReviewForAdvisory", () => { const env = aiEnv(async () => { throw new Error("claude CLI not found"); }); (env as unknown as { AI_PROVIDER: string; CLAUDE_AI_MODEL: string }).AI_PROVIDER = "claude-code"; (env as unknown as { AI_PROVIDER: string; CLAUDE_AI_MODEL: string }).CLAUDE_AI_MODEL = "claude-sonnet-4-6"; - const result = await runAiReviewForAdvisory(env, { settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", pr, author: "alice", confirmedContributor: true }); + const result = await runAiReviewForAdvisory(env, { mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", pr, author: "alice", confirmedContributor: true }); expect(result).toMatchObject({ cacheable: false, findings: [expect.objectContaining({ code: "ai_review_inconclusive" })], @@ -176,7 +195,7 @@ describe("runAiReviewForAdvisory", () => { it("records explicit self-host reviewer labels when models are omitted or providers are unknown", async () => { const env = aiEnv(async () => { throw new Error("codex unavailable"); }); (env as unknown as { AI_PROVIDER: string }).AI_PROVIDER = " CODEX , unknown-provider "; - const result = await runAiReviewForAdvisory(env, { settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", pr, author: "alice", confirmedContributor: true }); + const result = await runAiReviewForAdvisory(env, { mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", pr, author: "alice", confirmedContributor: true }); expect(result).toMatchObject({ cacheable: false, findings: [expect.objectContaining({ code: "ai_review_inconclusive" })], @@ -193,7 +212,7 @@ describe("runAiReviewForAdvisory", () => { OLLAMA_AI_MODEL: "llama3.1", AI_REVIEW_PLAN: { reviewers: [{ model: "anthropic", fallback: "ollama" }], combine: "single" }, }); - const result = await runAiReviewForAdvisory(env, { settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", pr, author: "alice", confirmedContributor: true }); + const result = await runAiReviewForAdvisory(env, { mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", pr, author: "alice", confirmedContributor: true }); expect(result).toMatchObject({ cacheable: false, findings: [expect.objectContaining({ code: "ai_review_inconclusive" })], @@ -204,7 +223,7 @@ describe("runAiReviewForAdvisory", () => { it("no-ops for a non-confirmed contributor under the gittensor pack and when there is no head SHA", async () => { const env = aiEnv(async () => ({ response: defectJson() })); - const base = { settings: { aiReviewMode: "block", gatePack: "gittensor" } as RepositorySettings, repoFullName: "acme/widgets", pr, author: "alice" }; + const base = { mode: "live" as const, settings: { aiReviewMode: "block", gatePack: "gittensor" } as RepositorySettings, repoFullName: "acme/widgets", pr, author: "alice" }; expect(await runAiReviewForAdvisory(env, { ...base, advisory: advisory(), confirmedContributor: false })).toBeUndefined(); const noSha = advisory(); delete (noSha as Partial).headSha; @@ -214,6 +233,7 @@ describe("runAiReviewForAdvisory", () => { it("runs a blocking AI review for a non-confirmed contributor under oss-anti-slop", async () => { const adv = advisory(); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: defectJson() })), { + mode: "live", settings: { aiReviewMode: "block", gatePack: "oss-anti-slop" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -232,6 +252,7 @@ describe("runAiReviewForAdvisory", () => { // is what lets it through — only the new flag. const adv = advisory(); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: notesOnlyJson() })), { + mode: "live", settings: { aiReviewMode: "advisory", gatePack: "gittensor", aiReviewAllAuthors: true , closeOwnerAuthors: false} as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -246,6 +267,7 @@ describe("runAiReviewForAdvisory", () => { it("appends an ai_consensus_defect finding in block mode when the models agree", async () => { const adv = advisory(); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: defectJson() })), { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -270,6 +292,7 @@ describe("runAiReviewForAdvisory", () => { return { response: model === "codex" ? defectJson() : notesOnlyJson() }; }) as unknown as () => Promise; const result = await runAiReviewForAdvisory(aiEnv(run), { + mode: "live", settings: { aiReviewMode: "block", aiReviewCombine: "single", @@ -294,6 +317,7 @@ describe("runAiReviewForAdvisory", () => { return { response: defectJson() }; }) as unknown as () => Promise; const result = await runAiReviewForAdvisory(aiEnv(run), { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, // no aiReviewCombine/OnMerge/Reviewers set advisory: adv, repoFullName: "acme/widgets", @@ -312,6 +336,7 @@ describe("runAiReviewForAdvisory", () => { const json = (confidence: number) => JSON.stringify({ assessment: "Likely crash.", blockers: ["Null dereference of a possibly-null value in src/a.ts."], nits: [], suggestions: [], confidence }); const run = (async (model: string) => ({ response: model === BEST_REVIEW_MODELS[0] ? json(0.95) : json(0.6) })) as unknown as () => Promise; await runAiReviewForAdvisory(aiEnv(run), { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -330,6 +355,7 @@ describe("runAiReviewForAdvisory", () => { const env = aiEnv(run); const captureSpy = vi.spyOn(sentryModule, "captureReviewFailure"); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -367,6 +393,7 @@ describe("runAiReviewForAdvisory", () => { const env = aiEnv((async () => ({ response: incoherent })) as unknown as () => Promise); const captureSpy = vi.spyOn(sentryModule, "captureReviewFailure"); await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -386,6 +413,7 @@ describe("runAiReviewForAdvisory", () => { // AND round-tripped on the returned cache payload so a cache hit can replay this blocker (#ai-review-split). const run = (async (model: string) => ({ response: model === BEST_REVIEW_MODELS[0] ? defectJson() : notesOnlyJson() })) as unknown as () => Promise; const result = await runAiReviewForAdvisory(aiEnv(run), { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -404,6 +432,7 @@ describe("runAiReviewForAdvisory", () => { const flagged = JSON.stringify({ assessment: "Likely crash.", blockers: ["Null deref in src/a.ts."], nits: [], suggestions: [], confidence: 0.45 }); const run = (async (model: string) => ({ response: model === BEST_REVIEW_MODELS[0] ? flagged : notesOnlyJson() })) as unknown as () => Promise; await runAiReviewForAdvisory(aiEnv(run), { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -425,6 +454,7 @@ describe("runAiReviewForAdvisory", () => { return { response: notesOnlyJson() }; }); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", @@ -449,6 +479,7 @@ describe("runAiReviewForAdvisory", () => { const adv = advisory(); await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -471,6 +502,7 @@ describe("runAiReviewForAdvisory", () => { }); await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", @@ -500,6 +532,7 @@ describe("runAiReviewForAdvisory", () => { }); await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", @@ -521,6 +554,7 @@ describe("runAiReviewForAdvisory", () => { }); await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", @@ -546,6 +580,7 @@ describe("runAiReviewForAdvisory", () => { it("returns advisory notes without a finding in advisory mode", async () => { const adv = advisory(); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: notesOnlyJson() })), { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -560,6 +595,7 @@ describe("runAiReviewForAdvisory", () => { it("returns undefined (no notes, no finding) when AI is disabled", async () => { const adv = advisory(); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: defectJson() }), false), { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -575,6 +611,7 @@ describe("runAiReviewForAdvisory", () => { const adv = advisory(); const captureSpy = vi.spyOn(sentryModule, "captureReviewFailure"); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: "" })), { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -609,6 +646,7 @@ describe("runAiReviewForAdvisory", () => { it("uses the non-cacheable block-mode inconclusive note when no reviewer returns public text", async () => { const adv = advisory(); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: "" })), { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -639,6 +677,7 @@ describe("runAiReviewForAdvisory", () => { expect((await claimAiReviewLock(env, "acme/widgets", 3, "sha3", "block")).acquired).toBe(true); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -661,6 +700,7 @@ describe("runAiReviewForAdvisory", () => { it("withholds unstructured AI text while holding the PR for manual review", async () => { const adv = advisory(); const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: "Looks coherent, but please verify the new cache branch before merging." })), { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -676,6 +716,7 @@ describe("runAiReviewForAdvisory", () => { it("preserves model nits when the model omits the assessment summary", async () => { const result = await runAiReviewForAdvisory(aiEnv(async () => ({ response: nitsWithoutAssessmentJson() })), { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", @@ -702,6 +743,7 @@ describe("runAiReviewForAdvisory", () => { const adv = advisory(); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "block", gatePack: "oss-anti-slop", aiReviewByok: true } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -728,6 +770,7 @@ describe("runAiReviewForAdvisory", () => { const fetchMock = vi.fn(async (_url: RequestInfo | URL, _init?: RequestInit) => new Response(JSON.stringify({ content: [{ type: "text", text: notesOnlyJson() }] }), { status: 200 })); vi.stubGlobal("fetch", fetchMock); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory", aiReviewByok: true } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", @@ -746,6 +789,7 @@ describe("runAiReviewForAdvisory", () => { const fetchMock = vi.fn(async (_url: RequestInfo | URL, _init?: RequestInit) => new Response(JSON.stringify({ content: [{ type: "text", text: notesOnlyJson() }] }), { status: 200 })); vi.stubGlobal("fetch", fetchMock); await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory", aiReviewByok: true, aiReviewProvider: "anthropic", aiReviewModel: "claude-from-yml" } as RepositorySettings, advisory: advisory(), repoFullName: "acme/widgets", @@ -764,6 +808,7 @@ describe("runAiReviewForAdvisory", () => { const fetchMock = vi.fn(async () => new Response("should not be called", { status: 200 })); vi.stubGlobal("fetch", fetchMock); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory", aiReviewByok: true, aiReviewProvider: "openai" } as RepositorySettings, // declared openai, stored anthropic → mismatch advisory: advisory(), repoFullName: "acme/widgets", @@ -780,6 +825,7 @@ describe("runAiReviewForAdvisory", () => { const adv = advisory(); const env = aiEnv(async () => ({ response: defectJson() })); const result = await runAiReviewForAdvisory({ ...env, DB: undefined } as unknown as Env, { + mode: "live", settings: { aiReviewMode: "block" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", diff --git a/test/unit/ai-slop.test.ts b/test/unit/ai-slop.test.ts index 323b8202c0..72da251282 100644 --- a/test/unit/ai-slop.test.ts +++ b/test/unit/ai-slop.test.ts @@ -385,6 +385,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { it("appends a single ai_slop_advisory finding when the model flags slop", async () => { const adv = advisory(); await runAiSlopForAdvisory(enabledEnv(async () => ({ response: slopJson({ band: "high" }) })), { + mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", @@ -401,7 +402,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { const noSha = advisory(); delete (noSha as Partial).headSha; const run = vi.fn(); - await runAiSlopForAdvisory(enabledEnv(run), { settings: noByok, advisory: noSha, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "low", confirmedContributor: true }); + await runAiSlopForAdvisory(enabledEnv(run), { mode: "live", settings: noByok, advisory: noSha, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "low", confirmedContributor: true }); expect(noSha.findings).toEqual([]); expect(run).not.toHaveBeenCalled(); }); @@ -409,6 +410,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { it("adds nothing when the model judges the change clean", async () => { const adv = advisory(); await runAiSlopForAdvisory(enabledEnv(async () => ({ response: slopJson({ band: "clean", rationale: "genuine", signals: [] }) })), { + mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", @@ -421,10 +423,18 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { expect(adv.findings).toEqual([]); }); + it("REGRESSION (#token-bleed-spend-gate): a paused mode never reaches the LLM call, even for a confirmed contributor", async () => { + const run = vi.fn(async () => ({ response: slopJson() })); + const adv = advisory(); + await runAiSlopForAdvisory(enabledEnv(run), { mode: "paused", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "high", confirmedContributor: true }); + expect(adv.findings).toEqual([]); + expect(run).not.toHaveBeenCalled(); + }); + it("is fail-safe: a thrown error (broken DB) yields no finding and never throws", async () => { const adv = advisory(); const env = { ...enabledEnv(async () => ({ response: slopJson() })), DB: undefined } as unknown as Env; - await expect(runAiSlopForAdvisory(env, { settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "high", confirmedContributor: true })).resolves.toBeUndefined(); + await expect(runAiSlopForAdvisory(env, { mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "high", confirmedContributor: true })).resolves.toBeUndefined(); expect(adv.findings).toEqual([]); }); @@ -442,6 +452,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { vi.stubGlobal("fetch", fetchMock); const adv = advisory(); await runAiSlopForAdvisory(env, { + mode: "live", settings: { aiReviewByok: true } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -471,6 +482,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { vi.stubGlobal("fetch", fetchMock); const adv = advisory(); await runAiSlopForAdvisory(env, { + mode: "live", settings: { aiReviewByok: true } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -499,7 +511,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { estimatedNeurons: 42, }); const adv = advisory(); - await runAiSlopForAdvisory(env, { settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "high", confirmedContributor: true }); + await runAiSlopForAdvisory(env, { mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "high", confirmedContributor: true }); expect(run).not.toHaveBeenCalled(); // no LLM call spent on the cache hit expect(adv.findings).toEqual([{ code: AI_SLOP_FINDING_CODE, title: "cached finding", severity: "warning", detail: "from cache" }]); @@ -518,7 +530,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { const repositoriesModule = await import("../../src/db/repositories"); const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 audit write error")); const adv = advisory(); - await runAiSlopForAdvisory(env, { settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "high", confirmedContributor: true }); + await runAiSlopForAdvisory(env, { mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "high", confirmedContributor: true }); expect(run).not.toHaveBeenCalled(); // still a cache hit despite the audit-write failure expect(adv.findings).toEqual([{ code: AI_SLOP_FINDING_CODE, title: "cached finding", severity: "warning", detail: "from cache" }]); @@ -533,6 +545,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { const env = enabledEnv(run); const adv = advisory(); await runAiSlopForAdvisory(env, { + mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", @@ -550,7 +563,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { const run = vi.fn(async () => ({ response: slopJson({ band: "elevated" }) })); const env = enabledEnv(run); const adv = advisory(); - await runAiSlopForAdvisory(env, { settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); + await runAiSlopForAdvisory(env, { mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); expect(run).toHaveBeenCalledTimes(1); // fresh call on the miss const fingerprint = await slopFingerprint({ deterministicBand: "elevated" }); @@ -559,7 +572,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { // A second call for the SAME head must now reuse the cache, not spend another LLM call. const adv2 = advisory(); - await runAiSlopForAdvisory(env, { settings: noByok, advisory: adv2, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); + await runAiSlopForAdvisory(env, { mode: "live", settings: noByok, advisory: adv2, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); expect(run).toHaveBeenCalledTimes(1); // still 1 — the second pass was a cache hit expect(adv2.findings).toEqual(adv.findings); }); @@ -568,7 +581,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { const run = vi.fn(async () => ({ response: slopJson({ band: "elevated" }) })); const budgetedEnv = createTestEnv({ AI: { run } as unknown as Ai, AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true", AI_DAILY_NEURON_BUDGET: "1" }); const adv = advisory(); - await runAiSlopForAdvisory(budgetedEnv, { settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); + await runAiSlopForAdvisory(budgetedEnv, { mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); expect(run).not.toHaveBeenCalled(); // quota_exceeded short-circuits before any model call expect(adv.findings).toEqual([]); @@ -578,7 +591,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { // Same head, budget now available — must still attempt the model instead of replaying a quota miss. const richEnv = createTestEnv({ AI: { run } as unknown as Ai, AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true", AI_DAILY_NEURON_BUDGET: "100000" }); const adv2 = advisory(); - await runAiSlopForAdvisory(richEnv, { settings: noByok, advisory: adv2, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); + await runAiSlopForAdvisory(richEnv, { mode: "live", settings: noByok, advisory: adv2, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); expect(run).toHaveBeenCalledTimes(1); }); @@ -603,7 +616,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { vi.stubGlobal("fetch", fetchMock); const adv = advisory(); - await runAiSlopForAdvisory(env, { settings: { aiReviewByok: true } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); + await runAiSlopForAdvisory(env, { mode: "live", settings: { aiReviewByok: true } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); // A fresh BYOK call was made (not the stale free-tier cache row, and not Workers AI). expect(fetchMock).toHaveBeenCalled(); @@ -617,7 +630,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { const repositoriesModule = await import("../../src/db/repositories"); const readSpy = vi.spyOn(repositoriesModule, "getCachedAiSlopAdvisory").mockRejectedValueOnce(new Error("D1 read error")); const adv = advisory(); - await runAiSlopForAdvisory(env, { settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); + await runAiSlopForAdvisory(env, { mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); expect(run).toHaveBeenCalledTimes(1); // degraded to a miss instead of throwing expect(adv.findings.map((f) => f.code)).toEqual([AI_SLOP_FINDING_CODE]); readSpy.mockRestore(); @@ -629,7 +642,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { const repositoriesModule = await import("../../src/db/repositories"); const writeSpy = vi.spyOn(repositoriesModule, "putCachedAiSlopAdvisory").mockRejectedValueOnce(new Error("D1 write error")); const adv = advisory(); - await runAiSlopForAdvisory(env, { settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); + await runAiSlopForAdvisory(env, { mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }); expect(adv.findings.map((f) => f.code)).toEqual([AI_SLOP_FINDING_CODE]); // swallowed, not thrown writeSpy.mockRestore(); @@ -651,7 +664,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => { }); const adv = advisory(); await expect( - runAiSlopForAdvisory(env, { settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }), + runAiSlopForAdvisory(env, { mode: "live", settings: noByok, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "elevated", confirmedContributor: true }), ).resolves.toBeUndefined(); // never throws, even with both the cache write AND its own audit write failing expect(adv.findings.map((f) => f.code)).toEqual([AI_SLOP_FINDING_CODE]); writeSpy.mockRestore(); diff --git a/test/unit/enrichment-wiring.test.ts b/test/unit/enrichment-wiring.test.ts index 5badb4f308..348a913ab2 100644 --- a/test/unit/enrichment-wiring.test.ts +++ b/test/unit/enrichment-wiring.test.ts @@ -137,6 +137,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE }); try { await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { @@ -208,6 +209,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE }); try { await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/off", pr: { number: 7, title: "t", body: "Fixes #42", linkedIssues: [42] }, @@ -256,6 +258,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE }); try { await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { number: 7, title: "t", body: "b" }, @@ -311,6 +314,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE }); try { await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { diff --git a/test/unit/grounding-wiring.test.ts b/test/unit/grounding-wiring.test.ts index 359c3068de..72b5b4baad 100644 --- a/test/unit/grounding-wiring.test.ts +++ b/test/unit/grounding-wiring.test.ts @@ -204,6 +204,7 @@ describe("review-grounding wired into the AI reviewer (flag GITTENSORY_REVIEW_GR }; try { const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { number: 7, title: "Add a feature", body: "Implements the thing." }, @@ -240,6 +241,7 @@ describe("review-grounding wired into the AI reviewer (flag GITTENSORY_REVIEW_GR }; try { const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/noinst", pr: { number: 7, title: "Add a feature", body: "x" }, diff --git a/test/unit/impact-map-processor-wiring.test.ts b/test/unit/impact-map-processor-wiring.test.ts index 119f495ea0..aa43751058 100644 --- a/test/unit/impact-map-processor-wiring.test.ts +++ b/test/unit/impact-map-processor-wiring.test.ts @@ -100,6 +100,7 @@ describe("impact map wired into runAiReviewForAdvisory (#2186)", () => { .bind("v1", "acme", "widgets", "src/review/caller.ts", 0, "code", "export function caller() { return computeThing(); }") .run(); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { number: 3, title: "Add computeThing", body: "Adds a helper." }, @@ -132,6 +133,7 @@ describe("impact map wired into runAiReviewForAdvisory (#2186)", () => { .bind("acme/widgets", 3, "src/review/impact-map.ts", "modified", 1, 0, 1, JSON.stringify({ patch: "@@\n+export function computeThing() {}" })) .run(); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { number: 3, title: "Add computeThing", body: "Adds a helper." }, @@ -157,6 +159,7 @@ describe("impact map wired into runAiReviewForAdvisory (#2186)", () => { .bind("acme/widgets", 3, "src/review/impact-map.ts", "modified", 1, 0, 1, JSON.stringify({ patch: "@@\n+export function computeThing() {}" })) .run(); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { number: 3, title: "Add computeThing", body: "Adds a helper." }, @@ -179,6 +182,7 @@ describe("impact map wired into runAiReviewForAdvisory (#2186)", () => { .bind("acme/widgets", 3, "src/review/impact-map.ts", "modified", 1, 0, 1, JSON.stringify({ patch: "@@\n+export function computeThing() {}" })) .run(); const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { number: 3, title: "Add computeThing", body: "Adds a helper." }, diff --git a/test/unit/linked-issue-satisfaction-run.test.ts b/test/unit/linked-issue-satisfaction-run.test.ts index fdb1979081..ce364d316b 100644 --- a/test/unit/linked-issue-satisfaction-run.test.ts +++ b/test/unit/linked-issue-satisfaction-run.test.ts @@ -316,18 +316,27 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" stubIssueFetch(); const run = vi.fn(); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "mallory", files, confirmedContributor: false, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "mallory", files, confirmedContributor: false, installationId: 1 }); expect(result).toBeNull(); expect(run).not.toHaveBeenCalled(); expect(adv.findings).toEqual([]); }); + it("REGRESSION (#token-bleed-spend-gate): a paused mode never reaches the LLM call, even for a confirmed contributor", async () => { + stubIssueFetch(); + const run = vi.fn(); + const adv = advisory(); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "paused", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + expect(result).toBeNull(); + expect(run).not.toHaveBeenCalled(); + }); + it("no-ops when the advisory has no head SHA", async () => { stubIssueFetch(); const noSha = advisory(); delete (noSha as Partial).headSha; const run = vi.fn(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: advisoryMode, advisory: noSha, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: advisoryMode, advisory: noSha, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toBeNull(); expect(run).not.toHaveBeenCalled(); }); @@ -337,7 +346,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const env = enabledEnv(run); vi.stubGlobal("fetch", vi.fn()); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr: { ...pr, linkedIssues: [] }, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr: { ...pr, linkedIssues: [] }, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toBeNull(); expect(run).not.toHaveBeenCalled(); }); @@ -346,7 +355,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" stubIssueFetch(); const run = vi.fn(async () => ({ response: satisfactionJson({ status: "addressed" }) })); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toMatchObject({ status: "addressed" }); }); @@ -355,7 +364,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const run = vi.fn(async () => ({ response: satisfactionJson({ status: "addressed" }) })); const adv = advisory(); const { body: _omit, ...prWithoutBody } = pr; - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr: prWithoutBody, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr: prWithoutBody, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toMatchObject({ status: "addressed" }); expect(run).toHaveBeenCalledTimes(1); }); @@ -364,7 +373,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" stubFetch((url) => (url.includes("/access_tokens") ? Response.json({ token: "t" }) : new Response("missing", { status: 404 }))); const run = vi.fn(); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toBeNull(); expect(run).not.toHaveBeenCalled(); }); @@ -373,7 +382,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" stubIssueFetch({ title: "", body: "" }); const run = vi.fn(); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toBeNull(); expect(run).not.toHaveBeenCalled(); }); @@ -383,7 +392,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const env = { ...enabledEnv(async () => ({ response: satisfactionJson() })), DB: undefined } as unknown as Env; const adv = advisory(); await expect( - runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }), + runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }), ).resolves.toBeNull(); expect(adv.findings).toEqual([]); }); @@ -393,7 +402,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" stubIssueFetch(); const run = vi.fn(async () => ({ response: satisfactionJson({ status: "unaddressed", confidence: 0.9, rationale: "The linked issue asks for an SSE stream; this PR adds an unrelated REST endpoint." }) })); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: blockMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: blockMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toMatchObject({ status: "unaddressed" }); expect(adv.findings).toHaveLength(1); @@ -408,7 +417,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" stubIssueFetch(); const run = vi.fn(async () => ({ response: satisfactionJson({ status: "unaddressed", confidence: 0.9 }) })); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toMatchObject({ status: "unaddressed" }); expect(adv.findings).toEqual([]); // advisory mode never restates the gap as a generic finding/Nit @@ -422,7 +431,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" stubIssueFetch(); const run = vi.fn(async () => ({ response: satisfactionJson({ status: "partial" }) })); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: blockMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: blockMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toMatchObject({ status: "partial" }); expect(adv.findings).toEqual([]); const gate = evaluateGateCheck(adv, { linkedIssueSatisfactionGateMode: "block" }); @@ -433,7 +442,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" stubIssueFetch(); const run = vi.fn(async () => ({ response: satisfactionJson({ status: "unaddressed", confidence: 0.1 }) })); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { settings: blockMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(enabledEnv(run), { mode: "live", settings: blockMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toBeNull(); expect(adv.findings).toEqual([]); const gate = evaluateGateCheck(adv, { linkedIssueSatisfactionGateMode: "block" }); @@ -459,7 +468,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" return new Response("not found", { status: 404 }); }); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(env, { settings: { linkedIssueSatisfactionGateMode: "advisory", aiReviewByok: true } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: { linkedIssueSatisfactionGateMode: "advisory", aiReviewByok: true } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toMatchObject({ status: "addressed" }); expect(workersRun).not.toHaveBeenCalled(); }); @@ -482,6 +491,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" }); const adv = advisory(); const result = await runLinkedIssueSatisfactionForAdvisory(env, { + mode: "live", settings: { linkedIssueSatisfactionGateMode: "advisory", aiReviewByok: true, aiReviewProvider: "anthropic" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -515,6 +525,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" }); const adv = advisory(); const result = await runLinkedIssueSatisfactionForAdvisory(env, { + mode: "live", settings: { linkedIssueSatisfactionGateMode: "advisory", aiReviewByok: true, aiReviewProvider: "openai" } as RepositorySettings, advisory: adv, repoFullName: "acme/widgets", @@ -542,7 +553,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" estimatedNeurons: 12, }); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(run).not.toHaveBeenCalled(); expect(result).toEqual({ status: "addressed", rationale: "cached: looks done" }); }); @@ -560,7 +571,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const repositoriesModule = await import("../../src/db/repositories"); const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockRejectedValueOnce(new Error("D1 audit write error")); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(run).not.toHaveBeenCalled(); // still a cache hit despite the audit-write failure expect(result).toEqual({ status: "addressed", rationale: "cached: looks done" }); auditSpy.mockRestore(); @@ -578,7 +589,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" }); const adv = advisory(); await expect( - runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }), + runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }), ).resolves.toMatchObject({ status: "addressed" }); // never throws, even with both the cache write AND its own audit write failing writeSpy.mockRestore(); auditSpy.mockRestore(); @@ -589,7 +600,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const run = vi.fn(async () => ({ response: satisfactionJson({ status: "addressed" }) })); const env = enabledEnv(run); const adv = advisory(); - await runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(run).toHaveBeenCalledTimes(1); const fingerprint = await processorFingerprint(); @@ -597,7 +608,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" expect(cached).toMatchObject({ status: "ok", result: { status: "addressed" } }); const adv2 = advisory(); - await runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv2, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv2, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(run).toHaveBeenCalledTimes(1); // still 1 — second pass was a cache hit }); @@ -607,7 +618,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const env = enabledEnv(run); const adv = advisory(); await expect( - runLinkedIssueSatisfactionForAdvisory(env, { settings: blockMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }), + runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: blockMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }), ).resolves.toMatchObject({ status: "addressed" }); stubIssueFetch({ body: "We now need a GraphQL subscription instead of an SSE stream." }); @@ -615,7 +626,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const changedAdvisory = advisory(); const changedPr = { ...pr, title: "Add SSE endpoint for the old issue", body: "Still only implements SSE." }; await expect( - runLinkedIssueSatisfactionForAdvisory(env, { settings: blockMode, advisory: changedAdvisory, repoFullName: "acme/widgets", pr: changedPr, author: "alice", files, confirmedContributor: true, installationId: 1 }), + runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: blockMode, advisory: changedAdvisory, repoFullName: "acme/widgets", pr: changedPr, author: "alice", files, confirmedContributor: true, installationId: 1 }), ).resolves.toMatchObject({ status: "unaddressed" }); expect(run).toHaveBeenCalledTimes(2); @@ -633,7 +644,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" estimatedNeurons: 5, }); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); // pr.linkedIssues is [1275] here, not 999 — must be a fresh call, not the stale row for issue #999. expect(run).toHaveBeenCalledTimes(1); expect(result).toMatchObject({ status: "addressed" }); @@ -644,12 +655,12 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const run = vi.fn(async () => ({ response: satisfactionJson({ status: "addressed" }) })); const budgetedEnv = createTestEnv({ AI: { run } as unknown as Ai, AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true", AI_DAILY_NEURON_BUDGET: "1" }); const adv = advisory(); - await runLinkedIssueSatisfactionForAdvisory(budgetedEnv, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + await runLinkedIssueSatisfactionForAdvisory(budgetedEnv, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(run).not.toHaveBeenCalled(); const richEnv = createTestEnv({ AI: { run } as unknown as Ai, AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true", AI_DAILY_NEURON_BUDGET: "100000" }); const adv2 = advisory(); - await runLinkedIssueSatisfactionForAdvisory(richEnv, { settings: advisoryMode, advisory: adv2, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + await runLinkedIssueSatisfactionForAdvisory(richEnv, { mode: "live", settings: advisoryMode, advisory: adv2, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(run).toHaveBeenCalledTimes(1); }); @@ -660,7 +671,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const repositoriesModule = await import("../../src/db/repositories"); const readSpy = vi.spyOn(repositoriesModule, "getCachedLinkedIssueSatisfaction").mockRejectedValueOnce(new Error("D1 read error")); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(run).toHaveBeenCalledTimes(1); expect(result).toMatchObject({ status: "addressed" }); readSpy.mockRestore(); @@ -673,7 +684,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)" const repositoriesModule = await import("../../src/db/repositories"); const writeSpy = vi.spyOn(repositoriesModule, "putCachedLinkedIssueSatisfaction").mockRejectedValueOnce(new Error("D1 write error")); const adv = advisory(); - const result = await runLinkedIssueSatisfactionForAdvisory(env, { settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); + const result = await runLinkedIssueSatisfactionForAdvisory(env, { mode: "live", settings: advisoryMode, advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, confirmedContributor: true, installationId: 1 }); expect(result).toMatchObject({ status: "addressed" }); writeSpy.mockRestore(); diff --git a/test/unit/rag-wiring.test.ts b/test/unit/rag-wiring.test.ts index 340faa353d..71c2a22f56 100644 --- a/test/unit/rag-wiring.test.ts +++ b/test/unit/rag-wiring.test.ts @@ -396,6 +396,7 @@ describe("RAG wired into the AI reviewer (flag GITTENSORY_REVIEW_RAG)", () => { title: "Gittensory advisory available", summary: "ok", findings: [], generatedAt: "2026-06-20T00:00:00.000Z", }; const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr: { number: 3, title: "Add helper", body: "Adds a helper." }, diff --git a/test/unit/repo-culture-profile-wiring.test.ts b/test/unit/repo-culture-profile-wiring.test.ts index c94b6f1a10..19db25e0df 100644 --- a/test/unit/repo-culture-profile-wiring.test.ts +++ b/test/unit/repo-culture-profile-wiring.test.ts @@ -232,6 +232,7 @@ describe("culture profile wired into the AI reviewer (flag GITTENSORY_REVIEW_CUL generatedAt: "2026-06-20T00:00:00.000Z", }; const result = await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: REPO, pr: { number: 3, title: "Add helper", body: "Adds a helper." }, @@ -266,6 +267,7 @@ describe("culture profile wired into the AI reviewer (flag GITTENSORY_REVIEW_CUL const extractSpy = vi.spyOn(cultureProfileModule, "extractRepoCultureProfile"); extractSpy.mockClear(); // discard any call history from an earlier test's spy on this same method await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: REPO, pr: { number: 4, title: "Add helper", body: "Adds a helper." }, @@ -298,6 +300,7 @@ describe("culture profile wired into the AI reviewer (flag GITTENSORY_REVIEW_CUL const extractSpy = vi.spyOn(cultureProfileModule, "extractRepoCultureProfile"); extractSpy.mockClear(); // discard any call history from an earlier test's spy on this same method await runAiReviewForAdvisory(env, { + mode: "live", settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: REPO, pr: { number: 5, title: "Add helper", body: "Adds a helper." }, diff --git a/test/unit/repository-settings-enforcement.test.ts b/test/unit/repository-settings-enforcement.test.ts index 153f3c8385..f66aa29651 100644 --- a/test/unit/repository-settings-enforcement.test.ts +++ b/test/unit/repository-settings-enforcement.test.ts @@ -89,6 +89,7 @@ describe("repository settings enforcement audit (#797)", () => { it("no-ops AI review when aiReviewMode is off", async () => { const advisory = missingIssueAdvisory(); const notes = await runAiReviewForAdvisory({} as Env, { + mode: "live", settings: settings({ aiReviewMode: "off" }), advisory, repoFullName: "owner/repo", diff --git a/test/unit/reputation-wiring.test.ts b/test/unit/reputation-wiring.test.ts index b14d99feb6..9120da5be0 100644 --- a/test/unit/reputation-wiring.test.ts +++ b/test/unit/reputation-wiring.test.ts @@ -50,7 +50,7 @@ function advisory(over: Partial = {}): Advisory { } const pr = { number: 3, title: "Add helper", body: "Adds a helper." }; -const baseArgs = { settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr, author: "burster", confirmedContributor: true }; +const baseArgs = { mode: "live" as const, settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/widgets", pr, author: "burster", confirmedContributor: true }; describe("isReputationEnabled", () => { it("is OFF for unset/false and ON for the truthy convention", () => { diff --git a/test/unit/visual-vision-wiring.test.ts b/test/unit/visual-vision-wiring.test.ts index 9e944fa498..fb9d7133cd 100644 --- a/test/unit/visual-vision-wiring.test.ts +++ b/test/unit/visual-vision-wiring.test.ts @@ -61,6 +61,7 @@ describe("runVisualVisionForAdvisory", () => { vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -73,12 +74,30 @@ describe("runVisualVisionForAdvisory", () => { expect(fetchMock).not.toHaveBeenCalled(); }); + it("REGRESSION (#token-bleed-spend-gate): a paused mode never reaches the vision call, even with a non-empty route list", async () => { + const env = byokEnv(); + stubShotsAndProvider(findingsResponse([{ path: "src/Button.tsx", body: "regressed" }])); + const adv = findingsHolder(); + await runVisualVisionForAdvisory(env, { + mode: "paused", + repoFullName, + pr, + author: "alice", + confirmedContributor: true, + settings: byokSettings(), + advisory: adv, + routes: [route({ path: "src/Button.tsx" })], + }); + expect(adv.findings).toEqual([]); + }); + it("handles a null author (ghost/deleted account) by treating it as an anonymous submitter, not a crash", async () => { const env = byokEnv(); const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: null, @@ -98,6 +117,7 @@ describe("runVisualVisionForAdvisory", () => { vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -127,6 +147,7 @@ describe("runVisualVisionForAdvisory", () => { vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "bob", @@ -145,6 +166,7 @@ describe("runVisualVisionForAdvisory", () => { vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -164,6 +186,7 @@ describe("runVisualVisionForAdvisory", () => { vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -183,6 +206,7 @@ describe("runVisualVisionForAdvisory", () => { vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -201,6 +225,7 @@ describe("runVisualVisionForAdvisory", () => { stubShotsAndProvider(findingsResponse([{ path: "/app", body: "The submit button is clipped on the right edge." }])); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -242,6 +267,7 @@ describe("runVisualVisionForAdvisory", () => { })); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -271,6 +297,7 @@ describe("runVisualVisionForAdvisory", () => { stubShotsAndProvider(findingsResponse([])); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -295,6 +322,7 @@ describe("runVisualVisionForAdvisory", () => { })); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -321,6 +349,7 @@ describe("runVisualVisionForAdvisory", () => { })); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -339,6 +368,7 @@ describe("runVisualVisionForAdvisory", () => { stubShotsAndProvider("I looked at the screenshots and everything seems fine, no JSON here."); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -356,6 +386,7 @@ describe("runVisualVisionForAdvisory", () => { stubShotsAndProvider(null); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -376,6 +407,7 @@ describe("runVisualVisionForAdvisory", () => { const adv = findingsHolder(); await expect( runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -414,6 +446,7 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", stubShots(); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -446,6 +479,7 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -468,6 +502,7 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", stubShots(); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -488,6 +523,7 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", stubShotsAndProvider(findingsResponse([{ path: "/app", body: "BYOK finding wins." }])); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -506,6 +542,7 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", stubShots(); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -523,6 +560,7 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", stubShots(); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -540,6 +578,7 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", stubShots(); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice", @@ -557,6 +596,7 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", vi.stubGlobal("fetch", fetchMock); const adv = findingsHolder(); await runVisualVisionForAdvisory(env, { + mode: "live", repoFullName, pr, author: "alice",