From ddb92b50d58b7ca8092b12083a2687d967b96625 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:24:07 -0700 Subject: [PATCH 1/3] feat(review): one-shot AI review cadence, configurable globally + per repo AI-generated review content (main review, slop advisory, linked-issue satisfaction) now freezes after its first pass by default -- no further automatic push/CI-completion/sweep trigger spends a fresh AI call, only an explicit maintainer retrigger does. Configurable via the new GITTENSORY_REVIEW_CONTINUOUS fleet-wide env default and the per-repo review.auto_review.cadence .gittensory.yml override (either direction), so self-hosters who want the traditional re-review-on-push behavior can opt back in. The deterministic gate is unaffected and always re-evaluates. Fixes #4636 --- .../src/routes/docs.privacy-security.tsx | 1 + apps/gittensory-ui/src/routes/docs.tuning.tsx | 12 + .../gittensory-engine/src/focus-manifest.ts | 20 ++ src/db/repositories.ts | 32 +++ src/env.d.ts | 9 + src/queue/processors.ts | 137 +++++++-- src/signals/focus-manifest.ts | 2 + test/unit/ai-slop-cache.test.ts | 29 +- test/unit/auto-review-config-matrix.test.ts | 9 + test/unit/focus-manifest.test.ts | 11 +- test/unit/gate-check-policy.test.ts | 20 +- .../linked-issue-satisfaction-cache.test.ts | 25 +- test/unit/queue.test.ts | 264 +++++++++++++++++- test/unit/signals-coverage.test.ts | 2 +- worker-configuration.d.ts | 4 +- wrangler.jsonc | 7 + 16 files changed, 551 insertions(+), 33 deletions(-) diff --git a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx index 780fa86183..f653ed75ff 100644 --- a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx +++ b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx @@ -98,6 +98,7 @@ GITTENSORY_REVIEW_SCREENSHOTS="true" # before/after visual capture f GITTENSORY_REVIEW_E2E_TESTS="true" # AI-generated E2E test coverage (needs features.e2eTests too) # Global (cron / endpoint) flags, not scoped by GITTENSORY_REVIEW_REPOS. +GITTENSORY_REVIEW_CONTINUOUS="true" # fleet-wide default: re-review on every push (else one-shot) GITTENSORY_REVIEW_OPS="true" # read-only anomaly scan + outcome stats endpoint GITTENSORY_REVIEW_SELFTUNE="true" # self-tightening tuning loop, never loosens GITTENSORY_REVIEW_PARITY_AUDIT="true" # shadow-record gate-decision parity readiness diff --git a/apps/gittensory-ui/src/routes/docs.tuning.tsx b/apps/gittensory-ui/src/routes/docs.tuning.tsx index f24df09925..7452eec78f 100644 --- a/apps/gittensory-ui/src/routes/docs.tuning.tsx +++ b/apps/gittensory-ui/src/routes/docs.tuning.tsx @@ -138,6 +138,18 @@ function Tuning() { needs its own features.e2eTests: true override in{" "} .gittensory.yml before the feature is active for it. Per-PR. +
  • + GITTENSORY_REVIEW_CONTINUOUS — fleet-wide default AI review re-trigger + cadence. Off by default (one-shot): AI-generated content (main review, slop advisory, + linked-issue satisfaction) is produced once per PR and never regenerated automatically + afterward — only an explicit maintainer retrigger (the PR-panel checkbox, or{" "} + @gittensory review as a maintainer) spends a fresh call. Truthy switches the + fleet default to continuous — every push/CI-completion/sweep re-runs AI content + generation. A repo's own review.auto_review.cadence in{" "} + .gittensory.yml always overrides this default, in either direction. Never + affects the deterministic gate (CI status, mergeability, static-rule blockers), which + always re-evaluates regardless. +
  • GITTENSORY_REVIEW_RAG — retrieval-augmented context: queries the codebase vector index for related code and docs (callers, related modules, existing conventions) diff --git a/packages/gittensory-engine/src/focus-manifest.ts b/packages/gittensory-engine/src/focus-manifest.ts index 0651c06e87..ab3f264e61 100644 --- a/packages/gittensory-engine/src/focus-manifest.ts +++ b/packages/gittensory-engine/src/focus-manifest.ts @@ -644,10 +644,25 @@ export type LabelingRule = { descriptionContains: string | null; }; +/** `review.auto_review.cadence` (#one-shot-review-cadence). `one_shot` = the AI-generated content (main review, + * slop advisory, linked-issue satisfaction) is produced once per PR and never automatically regenerated + * afterward — not on a new push, not on CI-check completion, not on a scheduled sweep tick; only an explicit + * maintainer retrigger (the PR-panel checkbox or `@gittensory review` as a maintainer) spends a fresh call. + * `continuous` = the traditional behavior — every trigger re-runs AI content generation, subject to each + * feature's own head-SHA cache. Orthogonal to `aiReviewMode`'s enforcement-strictness axis (off/advisory/ + * block) — the deterministic gate (CI status, mergeability, static-rule blockers) is NEVER affected by this + * and always re-evaluates on every pass regardless of cadence. */ +export const AI_REVIEW_CADENCES = ["one_shot", "continuous"] as const; +export type AiReviewCadence = (typeof AI_REVIEW_CADENCES)[number]; + /** Per-repo AI review eligibility knobs under `review.auto_review`. Unset fields are byte-identical defaults. */ export type AutoReviewConfig = { /** `review.auto_review.skip_drafts`: when true, draft PRs skip AI review. null (default) ⇒ drafts reviewed as today. (#2038) */ skipDrafts: boolean | null; + /** `review.auto_review.cadence`: per-repo override of the AI review re-trigger cadence. null (default) ⇒ + * inherit the operator's fleet-wide GITTENSORY_REVIEW_CONTINUOUS default (itself "one_shot" when unset). + * (#one-shot-review-cadence) */ + cadence: AiReviewCadence | null; /** `review.auto_review.ignore_authors`: author-login globs whose PRs skip AI review. Empty ⇒ every author. (#2039) */ ignoreAuthors: string[]; /** `review.auto_review.ignore_title_keywords`: case-insensitive title substrings that skip AI review. Empty ⇒ no skip. (#2040) */ @@ -677,6 +692,7 @@ export const EMPTY_MAX_FINDINGS_CONFIG: MaxFindingsConfig = { blockers: null, ni export const EMPTY_AUTO_REVIEW_CONFIG: AutoReviewConfig = { skipDrafts: null, + cadence: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], @@ -2299,6 +2315,7 @@ function overlayMaxFindingsConfig(base: MaxFindingsConfig, override: MaxFindings function overlayAutoReviewConfig(base: AutoReviewConfig, override: AutoReviewConfig): AutoReviewConfig { return { skipDrafts: pickOverlayNullable(override.skipDrafts, base.skipDrafts), + cadence: pickOverlayNullable(override.cadence, base.cadence), ignoreAuthors: pickOverlayStringList(override.ignoreAuthors, base.ignoreAuthors), ignoreTitleKeywords: pickOverlayStringList(override.ignoreTitleKeywords, base.ignoreTitleKeywords), skipLabels: pickOverlayStringList(override.skipLabels, base.skipLabels), @@ -2493,6 +2510,7 @@ function parseReviewLabelingRules(value: JsonValue | undefined, warnings: string function autoReviewPresent(config: AutoReviewConfig): boolean { return ( config.skipDrafts !== null || + config.cadence !== null || config.ignoreAuthors.length > 0 || config.ignoreTitleKeywords.length > 0 || config.skipLabels.length > 0 || @@ -2514,6 +2532,7 @@ function parseAutoReviewConfig(value: JsonValue | undefined, warnings: string[]) const record = value as Record; return { skipDrafts: normalizeOptionalBoolean(record.skip_drafts, "review.auto_review.skip_drafts", warnings), + cadence: normalizeOptionalEnum(record.cadence, "review.auto_review.cadence", AI_REVIEW_CADENCES, warnings), ignoreAuthors: parseManifestGlobList(record.ignore_authors, "review.auto_review.ignore_authors", warnings), ignoreTitleKeywords: parseAutoReviewTitleKeywords(record.ignore_title_keywords, warnings), skipLabels: parseAutoReviewSkipLabels(record.skip_labels, warnings), @@ -2937,6 +2956,7 @@ export function reviewConfigToJson(review: FocusManifestReviewConfig): JsonValue if (autoReviewPresent(review.autoReview)) { const autoReview: Record = {}; if (review.autoReview.skipDrafts !== null) autoReview.skip_drafts = review.autoReview.skipDrafts; + if (review.autoReview.cadence !== null) autoReview.cadence = review.autoReview.cadence; if (review.autoReview.ignoreAuthors.length > 0) autoReview.ignore_authors = [...review.autoReview.ignoreAuthors]; if (review.autoReview.ignoreTitleKeywords.length > 0) autoReview.ignore_title_keywords = [...review.autoReview.ignoreTitleKeywords]; if (review.autoReview.skipLabels.length > 0) autoReview.skip_labels = [...review.autoReview.skipLabels]; diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 2d3491c7c1..842b94203b 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -4759,6 +4759,19 @@ export async function putCachedAiSlopAdvisory( .run(); } +/** #one-shot-review-cadence: does at least one slop-advisory row exist for this PR, regardless of head SHA? + * Consulted ONLY when the resolved AI review cadence is "one_shot" — an existing row means this PR already + * had its one-shot slop pass, so a later automatic trigger (push/CI-completion/sweep) must not spend another + * LLM call. Existence-only (no reuse-for-display): mirrors the pre-existing commitThresholdReached silent-skip + * precedent for this same feature, which also does not resurface a prior finding once paused. */ +export async function hasPublishedAiSlopAdvisory(env: Env, repoFullName: string, pullNumber: number): Promise { + const row = await env.DB + .prepare("SELECT 1 AS present FROM ai_slop_cache WHERE repo_full_name = ? AND pull_number = ? LIMIT 1") + .bind(repoFullName, pullNumber) + .first<{ present: number }>(); + return Boolean(row); +} + /** #linked-issue-satisfaction-cache: the stored linked-issue satisfaction result for (repo, pull, head SHA, * linked issue number), or null on a miss. Mirrors getCachedAiSlopAdvisory -- every stored row is * unconditionally durable (no cacheable/allowNonCacheable/maxAgeMs dimension). A nullish head SHA is always a @@ -4814,6 +4827,25 @@ export async function putCachedLinkedIssueSatisfaction( .run(); } +/** #one-shot-review-cadence: does a linked-issue satisfaction row exist for this PR + linked issue number, + * regardless of head SHA? Consulted ONLY when the resolved AI review cadence is "one_shot", mirroring + * hasPublishedAiSlopAdvisory. Scoped ADDITIONALLY to linkedIssueNumber (not just the PR) — a PR's primary + * linked issue can change between passes (see linked_issue_satisfaction_cache's own doc comment), and a + * newly-linked issue has never been assessed, so it must still get its own first pass under one-shot mode + * rather than being silently blocked by an unrelated issue's prior assessment. */ +export async function hasPublishedLinkedIssueSatisfaction( + env: Env, + repoFullName: string, + pullNumber: number, + linkedIssueNumber: number, +): Promise { + const row = await env.DB + .prepare("SELECT 1 AS present FROM linked_issue_satisfaction_cache WHERE repo_full_name = ? AND pull_number = ? AND linked_issue_number = ? LIMIT 1") + .bind(repoFullName, pullNumber, linkedIssueNumber) + .first<{ present: number }>(); + return Boolean(row); +} + /** #4499 (grounding-file-content-cache): the stored file content for (repo, path, head SHA), or null on a * miss. Unlike linked_issue_satisfaction_cache, every stored row is durable with NO input-fingerprint * dimension -- file content at an immutable head SHA has exactly one correct value, so a hit is always safe diff --git a/src/env.d.ts b/src/env.d.ts index fb965729e5..b3344af813 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -259,6 +259,15 @@ declare global { * E2E test coverage feature. Default OFF — unset/false the feature is never active for any repo regardless * of a per-repo `features.e2eTests` override. */ GITTENSORY_REVIEW_E2E_TESTS?: string; + /** #one-shot-review-cadence: the operator's FLEET-WIDE default for AI review re-trigger cadence, consulted + * only when a repo's `.gittensory.yml review.auto_review.cadence` is unset (a per-repo value always wins + * regardless of this flag — see resolveAiReviewCadence). Default OFF (unset/false) ⇒ "one_shot": the + * AI-generated content (main review, slop advisory, linked-issue satisfaction) freezes after its first + * pass for every repo, and only an explicit maintainer retrigger spends a fresh call. Truthy ⇒ + * "continuous": the traditional behavior where every push/CI-completion/sweep trigger re-runs AI content + * generation, for operators who prefer that over one-shot. Never affects the deterministic gate (CI + * status, mergeability, static-rule blockers), which always re-evaluates regardless of this flag. */ + GITTENSORY_REVIEW_CONTINUOUS?: string; /** Convergence (reputation): when truthy, the INTERNAL-only ported submitter-reputation signal extends the * AI-spend gate — a new / burst / low-reputation submitter is downgraded to a deterministic-only review * (the AI neurons are skipped), and the per-(project, submitter) outcome is recorded after the gate diff --git a/src/queue/processors.ts b/src/queue/processors.ts index e4e6cff6ac..32ad3aba8c 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -52,8 +52,10 @@ import { markAiReviewPublished, getCachedAiSlopAdvisory, putCachedAiSlopAdvisory, + hasPublishedAiSlopAdvisory, getCachedLinkedIssueSatisfaction, putCachedLinkedIssueSatisfaction, + hasPublishedLinkedIssueSatisfaction, markPullRequestsRegated, markPullRequestsBacklogConvergenceRegated, markPullRequestReviewsInvalidated, @@ -430,6 +432,7 @@ import { resolveReviewPromptOverrides, resolveReviewMemoryManifestToggle, resolveReviewVisualConfig, + type AiReviewCadence, type FocusManifestFinding, type FocusManifest, type ReviewPathInstruction, @@ -6819,6 +6822,19 @@ export function shouldRunSlopAiAdvisory( return settings.slopAiAdvisory && settings.slopGateMode !== "off"; } +/** #one-shot-review-cadence: resolve the effective AI review re-trigger cadence. The per-repo + * `review.auto_review.cadence` manifest field (`configuredCadence`, already resolved by + * resolveReviewAutoReviewConfig) always wins when set; otherwise falls back to the operator's fleet-wide + * GITTENSORY_REVIEW_CONTINUOUS default. Both unset ⇒ "one_shot" — see AutoReviewConfig["cadence"]'s own doc + * comment for the full semantics. */ +export function resolveAiReviewCadence( + env: { GITTENSORY_REVIEW_CONTINUOUS?: string | undefined }, + configuredCadence: AiReviewCadence | null, +): AiReviewCadence { + if (configuredCadence !== null) return configuredCadence; + return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_CONTINUOUS ?? "") ? "continuous" : "one_shot"; +} + function shouldProcessPullRequestPublicSurface( eventName: string, action: string | undefined, @@ -8877,6 +8893,13 @@ async function maybePublishPrPublicSurface( return undefined; const reviewManifest = await loadRepoFocusManifest(env, repoFullName).catch(() => null); const autoReviewConfig = resolveReviewAutoReviewConfig(reviewManifest); + // #one-shot-review-cadence: resolved once, up front, so all three AI dispatch sites below (slop, + // linked-issue satisfaction, main review) see the same answer. An explicit maintainer retrigger + // (forceAiReview, set by the PR-panel checkbox or a maintainer's `@gittensory review`) always bypasses + // one-shot mode regardless of cadence -- that is the whole point of "one-shot until you ask again." + const oneShotCadenceActive = + resolveAiReviewCadence(env, autoReviewConfig.cadence) === "one_shot" && + webhook.forceAiReview !== true; const reviewEligibility = decideReviewEligibility({ authorLogin: author, ignoreAuthors: autoReviewConfig.ignoreAuthors, @@ -9560,42 +9583,81 @@ async function maybePublishPrPublicSurface( // AI-assisted slop advisory (#533, opt-in). Reuses the already-fetched files; appends at most one // advisory-only finding. Deliberately does NOT update slopRisk — only the deterministic core blocks. if (shouldRunSlopAiAdvisory(settings)) { - // #ai-slop-repeat-spend: same commit-threshold cap ai_review already applies (auto_pause_after_reviewed_commits) - // — a PR the sweep keeps re-visiting stops getting a fresh slop advisory once it's been reviewed enough - // times, instead of re-attempting (and re-touching the shared neuron/provider budget) on every single pass. - const slopReviewedCommitCount = await countPublishedAiReviewHeads(env, repoFullName, pr.number).catch(() => 0); - await runAiSlopForAdvisory(env, { + // #one-shot-review-cadence: a repeat automatic trigger (push/CI-completion/sweep) under one-shot mode + // must not spend another slop LLM call once this PR has already had ITS one-shot slop pass -- silent + // skip (mirrors commitThresholdReached just below: no reuse-for-display, matching the pre-existing + // precedent for this same feature). An explicit maintainer retrigger already unset oneShotCadenceActive + // above, so it always reaches the fresh call regardless of prior passes. + const slopOneShotSkip = + oneShotCadenceActive && + (await hasPublishedAiSlopAdvisory(env, repoFullName, pr.number).catch(() => false)); + if (slopOneShotSkip) { + await recordAuditEvent(env, { + eventType: "github_app.ai_slop_one_shot_skip", + actor: author, + targetKey: `${repoFullName}#${pr.number}`, + outcome: "completed", + detail: "one-shot review cadence: this PR already had its slop advisory pass; not spending a fresh call", + metadata: { repoFullName, headSha: advisory.headSha ?? null }, + }).catch(() => undefined); + } else { + // #ai-slop-repeat-spend: same commit-threshold cap ai_review already applies (auto_pause_after_reviewed_commits) + // — a PR the sweep keeps re-visiting stops getting a fresh slop advisory once it's been reviewed enough + // times, instead of re-attempting (and re-touching the shared neuron/provider budget) on every single pass. + const slopReviewedCommitCount = await countPublishedAiReviewHeads(env, repoFullName, pr.number).catch(() => 0); + await runAiSlopForAdvisory(env, { + mode, + settings, + advisory, + repoFullName, + pr, + author, + files: slopFiles, + deterministicBand: slop.band, + confirmedContributor, + commitThresholdReached: isAutoReviewCommitThresholdReached(autoReviewConfig, slopReviewedCommitCount), + }); + } + } + } + // Linked-issue satisfaction assessment (#1961/#3906, opt-in via linkedIssueSatisfactionGateMode). Assesses + // only the PR's primary linked issue -- see runLinkedIssueSatisfactionForAdvisory's own doc comment for + // the multi-linked-issue rationale. `off` (default) short-circuits before any fetch or model call, so this + // is byte-identical to before this feature existed for every repo that hasn't opted in. (Declared/hoisted + // to function scope above, alongside gateEvaluation, since it is consumed later outside this try block.) + if (settings.linkedIssueSatisfactionGateMode !== "off" && pr.linkedIssues.length > 0) { + // #one-shot-review-cadence: mirrors the slop advisory's skip above, scoped to the PR's PRIMARY linked + // issue (matching runLinkedIssueSatisfactionForAdvisory's own "assesses only the first linked issue" + // contract) -- a newly-linked issue never assessed before still gets its own first pass even when the + // PR itself already had a satisfaction pass for a DIFFERENT (now-superseded) linked issue. + const primaryLinkedIssueNumber = pr.linkedIssues[0]; + const linkedIssueOneShotSkip = + oneShotCadenceActive && + primaryLinkedIssueNumber !== undefined && + (await hasPublishedLinkedIssueSatisfaction(env, repoFullName, pr.number, primaryLinkedIssueNumber).catch(() => false)); + if (linkedIssueOneShotSkip) { + await recordAuditEvent(env, { + eventType: "github_app.linked_issue_satisfaction_one_shot_skip", + actor: author, + targetKey: `${repoFullName}#${pr.number}`, + outcome: "completed", + detail: "one-shot review cadence: this PR's linked issue already had its satisfaction pass; not spending a fresh call", + metadata: { repoFullName, headSha: advisory.headSha ?? null }, + }).catch(() => undefined); + } else { + linkedIssueSatisfaction = await runLinkedIssueSatisfactionForAdvisory(env, { mode, settings, advisory, repoFullName, pr, author, - files: slopFiles, - deterministicBand: slop.band, + files: await getReviewFiles(), confirmedContributor, - commitThresholdReached: isAutoReviewCommitThresholdReached(autoReviewConfig, slopReviewedCommitCount), + installationId, }); } } - // Linked-issue satisfaction assessment (#1961/#3906, opt-in via linkedIssueSatisfactionGateMode). Assesses - // only the PR's primary linked issue -- see runLinkedIssueSatisfactionForAdvisory's own doc comment for - // the multi-linked-issue rationale. `off` (default) short-circuits before any fetch or model call, so this - // is byte-identical to before this feature existed for every repo that hasn't opted in. (Declared/hoisted - // 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, - pr, - author, - files: await getReviewFiles(), - confirmedContributor, - installationId, - }); - } // Focus-manifest policy (#555, opt-in via manifestPolicyGateMode). Reload the CACHED manifest (the // settings resolver discards the raw manifest, but loadRepoFocusManifest is cached so this is cheap), // recompute the guidance over the PR's changed files, and push ONLY the three enforceable policy @@ -9843,10 +9905,20 @@ async function maybePublishPrPublicSurface( isReputationEnabled(env) && isConvergenceRepoAllowed(env, repoFullName) ? await shouldSkipAiForReputation(env, { project: repoFullName, submitter: author }) : undefined; + // #one-shot-review-cadence: only even attempts the lookup when the review would otherwise be eligible to + // run fresh this pass (mirrors how the frozen/paused branches below are similarly mutually exclusive) -- + // a PR that's blacklisted/frozen/already-skipped for another reason never shows AI content at all today, + // and one-shot mode must not change that. A non-null result here means this PR already had its one-shot + // main-review pass, so the fresh call below must be skipped and this reused instead. + const oneShotPriorReview = + oneShotCadenceActive && !authorBlacklisted && !isFrozenForManualReview && !autoReviewSkipReason + ? await getLatestPublishedAiReview(env, repoFullName, pr.number, settings.aiReviewMode).catch(() => null) + : null; const aiReviewWillRun = !authorBlacklisted && !isFrozenForManualReview && !autoReviewSkipReason && + !oneShotPriorReview && (await shouldStartAiReviewForAdvisory(env, { settings, advisory, @@ -9899,6 +9971,19 @@ async function maybePublishPrPublicSurface( metadata: { deliveryId: webhook.deliveryId, repoFullName, headSha: advisory.headSha ?? null }, }).catch(() => undefined); } + } else if (oneShotPriorReview && hasPublicReviewAssessment(oneShotPriorReview.notes)) { + advisory.findings.push(...oneShotPriorReview.findings); + aiReview = oneShotPriorReview; + aiReviewWasReused = true; + incr("gittensory_ai_review_one_shot_reuse_total"); + await recordAuditEvent(env, { + eventType: "github_app.ai_review_one_shot_reuse", + actor: author, + targetKey: `${repoFullName}#${pr.number}`, + outcome: "completed", + detail: "one-shot review cadence: reused the last published AI review instead of spending a fresh call", + metadata: { deliveryId: webhook.deliveryId, repoFullName, headSha: advisory.headSha ?? null }, + }).catch(() => undefined); } // Review-evasion protection (#review-evasion-protection): durably record that a review pass is starting // for this EXACT head BEFORE any cost-bearing AI-review work begins (including the reviewing placeholder diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index 8f131310fd..ad91b57f7c 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -4,6 +4,7 @@ * `src/` modules (`classifyChangedFile`, `mergeContributorBlacklists`, etc.). */ export { + AI_REVIEW_CADENCES, COMMENT_VERBOSITY_LEVELS, CONVERGED_FEATURE_KEYS, E2E_TEST_DELIVERY_MODES, @@ -33,6 +34,7 @@ export { reviewRecapConfigToJson, maintainerRecapConfigToJson, settingsOverrideToJson, + type AiReviewCadence, type AutoReviewConfig, type CommentVerbosity, type ConvergedFeatureKey, diff --git a/test/unit/ai-slop-cache.test.ts b/test/unit/ai-slop-cache.test.ts index fdb029bd2f..114b7913d9 100644 --- a/test/unit/ai-slop-cache.test.ts +++ b/test/unit/ai-slop-cache.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { getCachedAiSlopAdvisory, putCachedAiSlopAdvisory } from "../../src/db/repositories"; +import { getCachedAiSlopAdvisory, hasPublishedAiSlopAdvisory, putCachedAiSlopAdvisory } from "../../src/db/repositories"; import { aiSlopCacheInputFingerprint } from "../../src/review/ai-slop-cache-input"; import { createTestEnv } from "../helpers/d1"; @@ -86,6 +86,33 @@ describe("AI slop advisory cache (#ai-slop-cache)", () => { }); }); +describe("hasPublishedAiSlopAdvisory (#one-shot-review-cadence)", () => { + it("is false when no row exists for the PR", async () => { + const env = createTestEnv(); + expect(await hasPublishedAiSlopAdvisory(env, "o/r", 20)).toBe(false); + }); + + it("is true once ANY row exists, regardless of head SHA -- existence-only, not scoped to the current head", async () => { + const env = createTestEnv(); + const fingerprint = await fp(); + await putCachedAiSlopAdvisory(env, "o/r", 21, "sha1", fingerprint, { status: "ok", band: "low", finding: null, estimatedNeurons: 4 }); + expect(await hasPublishedAiSlopAdvisory(env, "o/r", 21)).toBe(true); + // A DIFFERENT, later head SHA for the SAME PR still reads as "already had its pass" -- one-shot mode's + // whole point is that a new push does not reset this. The lookup carries no headSha parameter at all, + // so a query for the SAME PR after a would-be new commit lands is unaffected by which SHA is now current. + await putCachedAiSlopAdvisory(env, "o/r", 21, "sha2", fingerprint, { status: "ok", band: "low", finding: null, estimatedNeurons: 4 }); + expect(await hasPublishedAiSlopAdvisory(env, "o/r", 21)).toBe(true); + }); + + it("scopes strictly to (repo, pull) -- a different PR or repo is unaffected", async () => { + const env = createTestEnv(); + const fingerprint = await fp(); + await putCachedAiSlopAdvisory(env, "o/r", 22, "sha1", fingerprint, { status: "ok", band: "low", finding: null, estimatedNeurons: 4 }); + expect(await hasPublishedAiSlopAdvisory(env, "o/r", 23)).toBe(false); // different PR + expect(await hasPublishedAiSlopAdvisory(env, "o/r2", 22)).toBe(false); // different repo + }); +}); + describe("aiSlopCacheInputFingerprint", () => { const promptInput = { title: "Tidy", diff --git a/test/unit/auto-review-config-matrix.test.ts b/test/unit/auto-review-config-matrix.test.ts index 9798a87b35..e7d373d4d7 100644 --- a/test/unit/auto-review-config-matrix.test.ts +++ b/test/unit/auto-review-config-matrix.test.ts @@ -20,6 +20,8 @@ describe("review.auto_review parse ↔ reviewConfigToJson round-trip (#2071)", ( const roundTripCases: Array<{ name: string; autoReview: Record }> = [ { name: "skip_drafts: true", autoReview: { skip_drafts: true } }, { name: "skip_drafts: false", autoReview: { skip_drafts: false } }, + { name: "cadence: one_shot", autoReview: { cadence: "one_shot" } }, + { name: "cadence: continuous", autoReview: { cadence: "continuous" } }, { name: "ignore_authors", autoReview: { ignore_authors: ["*[bot]", "dependabot[bot]"] } }, { name: "ignore_title_keywords", autoReview: { ignore_title_keywords: ["WIP", "draft"] } }, { name: "skip_labels", autoReview: { skip_labels: ["do-not-review", "wip"] } }, @@ -33,6 +35,7 @@ describe("review.auto_review parse ↔ reviewConfigToJson round-trip (#2071)", ( name: "all knobs together", autoReview: { skip_drafts: true, + cadence: "one_shot", ignore_authors: ["*[bot]"], ignore_title_keywords: ["WIP"], skip_labels: ["do-not-review"], @@ -80,6 +83,12 @@ describe("review.auto_review malformed config (#2071)", () => { expect(bad.review.autoReview.autoPauseAfterReviewedCommits).toBeNull(); expect(bad.warnings.some((w) => /auto_pause_after_reviewed_commits.*non-negative integer/.test(w))).toBe(true); }); + + it("warns on an invalid cadence value and drops the knob (#one-shot-review-cadence)", () => { + const bad = parseFocusManifest({ review: { auto_review: { cadence: "sometimes" } } }); + expect(bad.review.autoReview.cadence).toBeNull(); + expect(bad.warnings.some((w) => /auto_review\.cadence.*must be one of/.test(w))).toBe(true); + }); }); describe("evaluateAutoReviewSkipReason predicate precedence (#2071)", () => { diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 8639685147..eb5d0bba5b 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -3686,14 +3686,22 @@ describe("overlayReviewConfig / review.shared_config (#2046)", () => { }); it("merges nested auto_review and partial field maps key-by-key", () => { - const base = parseReviewConfigMapping({ auto_review: { skip_drafts: true, ignore_authors: ["bot"] }, fields: { relatedWork: false } }, []); + const base = parseReviewConfigMapping({ auto_review: { skip_drafts: true, cadence: "one_shot", ignore_authors: ["bot"] }, fields: { relatedWork: false } }, []); const override = parseReviewConfigMapping({ auto_review: { ignore_authors: ["dependabot"] }, fields: { openPrQueue: true } }, []); const merged = overlayReviewConfig(base, override); expect(merged.autoReview.skipDrafts).toBe(true); + // #one-shot-review-cadence: override left cadence unset, so the base's value fills the gap (pickOverlayNullable). + expect(merged.autoReview.cadence).toBe("one_shot"); expect(merged.autoReview.ignoreAuthors).toEqual(["dependabot"]); expect(merged.fields).toEqual({ relatedWork: false, openPrQueue: true }); }); + it("lets an explicit override cadence win over the base's (#one-shot-review-cadence)", () => { + const base = parseReviewConfigMapping({ auto_review: { cadence: "one_shot" } }, []); + const override = parseReviewConfigMapping({ auto_review: { cadence: "continuous" } }, []); + expect(overlayReviewConfig(base, override).autoReview.cadence).toBe("continuous"); + }); + it("preserves sharedConfigSource from the override when set", () => { const base = parseReviewConfigMapping({ tone: "house" }, []); const override = { ...parseReviewConfigMapping({ profile: "assertive" }, []), sharedConfigSource: "_shared/.gittensory.yml" }; @@ -3722,6 +3730,7 @@ describe("review.auto_review (#1954 / #2038–#2041)", () => { }); expect(m.review.autoReview).toEqual({ skipDrafts: true, + cadence: null, ignoreAuthors: ["*[bot]", "dependabot[bot]"], ignoreTitleKeywords: ["WIP", "draft"], skipLabels: ["do-not-review", "wip"], diff --git a/test/unit/gate-check-policy.test.ts b/test/unit/gate-check-policy.test.ts index c2011a12ce..9815779dc3 100644 --- a/test/unit/gate-check-policy.test.ts +++ b/test/unit/gate-check-policy.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { generateKeyPairSync } from "node:crypto"; import { clearInstallationTokenCacheForTest } from "../../src/github/app"; -import { buildAuthorizedPrActionAdvisory, gateCheckPolicy, resolveLinkedIssueAuthorLogins, shouldCollectLinkedIssueEvidence, shouldCollectSlopEvidence, shouldRefreshFilesForPreMergeChecks, shouldRunSlopAiAdvisory } from "../../src/queue/processors"; +import { buildAuthorizedPrActionAdvisory, gateCheckPolicy, resolveAiReviewCadence, resolveLinkedIssueAuthorLogins, shouldCollectLinkedIssueEvidence, shouldCollectSlopEvidence, shouldRefreshFilesForPreMergeChecks, shouldRunSlopAiAdvisory } from "../../src/queue/processors"; import { createTestEnv } from "../helpers/d1"; import { upsertIssueFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories"; import { evaluateGateCheck } from "../../src/rules/advisory"; @@ -512,6 +512,24 @@ describe("merge-readiness evidence collection (#551)", () => { expect(shouldRunSlopAiAdvisory(settings({ slopGateMode: "off", mergeReadinessGateMode: "advisory", slopAiAdvisory: true }))).toBe(false); expect(shouldRunSlopAiAdvisory(settings({ slopGateMode: "advisory", slopAiAdvisory: false }))).toBe(false); }); + + describe("resolveAiReviewCadence (#one-shot-review-cadence)", () => { + it("lets an explicit configured per-repo cadence win over the fleet env default, in both directions", () => { + expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "true" }, "one_shot")).toBe("one_shot"); + expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "false" }, "continuous")).toBe("continuous"); + }); + + it('falls back to "continuous" when unconfigured and the fleet env flag is truthy', () => { + expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "true" }, null)).toBe("continuous"); + expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "1" }, null)).toBe("continuous"); + }); + + it('defaults to "one_shot" when unconfigured and the fleet env flag is unset or falsy', () => { + expect(resolveAiReviewCadence({}, null)).toBe("one_shot"); + expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "false" }, null)).toBe("one_shot"); + expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "nonsense" }, null)).toBe("one_shot"); + }); + }); }); describe("merge-readiness composite gate (#551)", () => { diff --git a/test/unit/linked-issue-satisfaction-cache.test.ts b/test/unit/linked-issue-satisfaction-cache.test.ts index 94d0b5d351..1bd89cf155 100644 --- a/test/unit/linked-issue-satisfaction-cache.test.ts +++ b/test/unit/linked-issue-satisfaction-cache.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { getCachedLinkedIssueSatisfaction, putCachedLinkedIssueSatisfaction } from "../../src/db/repositories"; +import { getCachedLinkedIssueSatisfaction, hasPublishedLinkedIssueSatisfaction, putCachedLinkedIssueSatisfaction } from "../../src/db/repositories"; import { linkedIssueSatisfactionCacheInputFingerprint } from "../../src/review/linked-issue-satisfaction-cache-input"; import { createTestEnv } from "../helpers/d1"; @@ -91,6 +91,29 @@ describe("linked-issue satisfaction cache (#1961/#3906)", () => { }); }); +describe("hasPublishedLinkedIssueSatisfaction (#one-shot-review-cadence)", () => { + it("is false when no row exists for the PR + linked issue number", async () => { + const env = createTestEnv(); + expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r", 20, 1)).toBe(false); + }); + + it("is true once ANY row exists for that issue number, regardless of head SHA", async () => { + const env = createTestEnv(); + const fingerprint = await fp(); + await putCachedLinkedIssueSatisfaction(env, "o/r", 21, "sha1", 5, fingerprint, { status: "ok", result: { status: "addressed", rationale: "r", confidence: 0.9 }, estimatedNeurons: 4 }); + expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r", 21, 5)).toBe(true); + }); + + it("scopes to (repo, pull, linkedIssueNumber) -- a newly-linked (never-assessed) issue still reads false even though the PR already has a pass for a DIFFERENT issue", async () => { + const env = createTestEnv(); + const fingerprint = await fp(); + await putCachedLinkedIssueSatisfaction(env, "o/r", 22, "sha1", 5, fingerprint, { status: "ok", result: { status: "addressed", rationale: "r", confidence: 0.9 }, estimatedNeurons: 4 }); + expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r", 22, 6)).toBe(false); // different (newly-linked) issue on the SAME PR + expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r", 23, 5)).toBe(false); // different PR + expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r2", 22, 5)).toBe(false); // different repo + }); +}); + describe("linkedIssueSatisfactionCacheInputFingerprint", () => { it("is stable for the same input", async () => { const a = await linkedIssueSatisfactionCacheInputFingerprint({ byok: false, provider: null, model: null }); diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index cb8e3edf17..bb5bd5eceb 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -53,6 +53,8 @@ import { upsertRepositoryFromGitHub, putCachedAiReview, markAiReviewPublished, + putCachedAiSlopAdvisory, + putCachedLinkedIssueSatisfaction, recordReviewSuppression, listReviewSuppressions, setGlobalAgentFrozen, @@ -3907,6 +3909,10 @@ describe("queue processors", () => { AI_DAILY_NEURON_BUDGET: "100000", }); await seedRegateChurnRepo(env); + // #one-shot-review-cadence: this test is about the non-cacheable-outcome cooldown specifically, not + // cadence -- opt into continuous so the SAME unchanged-head assertions below exercise that mechanism in + // isolation, unaffected by the one_shot default now suppressing repeat automatic passes for a different reason. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Quiet PR", state: "open", user: { login: "contributor" }, head: { sha: "a60" }, labels: [], body: "Closes #1" }); await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 60, status: "complete", reviewsSyncedAt: new Date().toISOString() }); vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { @@ -5276,6 +5282,9 @@ describe("queue processors", () => { AI_DAILY_NEURON_BUDGET: "100000", }); await seedRegateChurnRepo(env); + // #one-shot-review-cadence: isolate this test to the cooldown-vs-real-state-change mechanism it's actually + // about (see the PR 60 test above for the identical rationale). + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 63, title: "Pushed PR", state: "open", user: { login: "contributor" }, head: { sha: "a63" }, labels: [], body: "Closes #1" }); await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 63, status: "complete", reviewsSyncedAt: new Date().toISOString() }); let liveHeadSha = "a63"; @@ -5447,6 +5456,8 @@ describe("queue processors", () => { AI_DAILY_NEURON_BUDGET: "100000", }); await seedRegateChurnRepo(env, { publicSurface: "comment_and_label" }); + // #one-shot-review-cadence: isolate this test to the unchanged-head/label-repair mechanism it's about. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 70, title: "Fix the retry loop", state: "open", user: { login: "contributor" }, head: { sha: "a70" }, labels: [], body: "Closes #1" }); await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 70, status: "complete", reviewsSyncedAt: new Date().toISOString() }); @@ -5587,6 +5598,8 @@ describe("queue processors", () => { AI_DAILY_NEURON_BUDGET: "100000", }); await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + // #one-shot-review-cadence: isolate this test to the real-code-change-triggers-fresh-review mechanism. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 72, title: "Evolving PR", state: "open", user: { login: "contributor" }, head: { sha: "a72" }, labels: [], body: "Closes #1" }); await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 72, status: "complete", reviewsSyncedAt: new Date().toISOString() }); @@ -5878,6 +5891,245 @@ describe("queue processors", () => { expect(bypassAudit?.outcome).toBe("completed"); }); + describe("one-shot AI review cadence (#one-shot-review-cadence)", () => { + it("default (one_shot, no manual-review label): a genuinely NEW push does not spend a fresh main-review AI call -- reuses the prior published review", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Fresh (should not happen under one-shot).", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + // No cadence override anywhere (no yml, no GITTENSORY_REVIEW_CONTINUOUS) -- one_shot is the codebase default. + await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 91, title: "One-shot PR", state: "open", user: { login: "contributor" }, head: { sha: "a91-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 91, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedAiReview(env, "JSONbored/gittensory", 91, "a91-v1", "block", { notes: "Original one-shot review.", reviewerCount: 1 }); + await markAiReviewPublished(env, "JSONbored/gittensory", 91, "a91-v1"); + + let publicCommentBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + // A genuinely NEW push -- a new head SHA, no manual-review label anywhere. + if (url.includes("/pulls/91/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/91")) return Response.json({ number: 91, title: "One-shot PR", state: "open", user: { login: "contributor" }, head: { sha: "a91-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a91-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a91-v2/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/91/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/91/comments") && method === "POST") { + publicCommentBody = 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: "agent-regate-pr", deliveryId: "one-shot-push", repoFullName: "JSONbored/gittensory", prNumber: 91, installationId: 123 }); + + expect(aiCalls).toBe(0); // the new head never bought a fresh AI review + expect(publicCommentBody).toContain("Original one-shot review."); + const reuseAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.ai_review_one_shot_reuse", "JSONbored/gittensory#91") + .first<{ outcome: string; detail: string }>(); + expect(reuseAudit?.outcome).toBe("completed"); + expect(reuseAudit?.detail).toContain("one-shot review cadence"); + }); + + it("default (one_shot): a PR with no prior review yet still gets its first pass -- one-shot never blocks the FIRST review", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "First pass.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 92, title: "Brand-new PR", state: "open", user: { login: "contributor" }, head: { sha: "a92" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 92, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + // Deliberately NO prior putCachedAiReview/markAiReviewPublished -- this PR has never been reviewed. + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/92/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/92")) return Response.json({ number: 92, title: "Brand-new PR", state: "open", user: { login: "contributor" }, head: { sha: "a92" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a92/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a92/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/92/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-first-pass", repoFullName: "JSONbored/gittensory", prNumber: 92, installationId: 123 }); + + expect(aiCalls).toBeGreaterThan(0); // the very first pass is never suppressed by one-shot mode + const reuseAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") + .bind("github_app.ai_review_one_shot_reuse", "JSONbored/gittensory#92") + .first<{ n: number }>(); + expect(reuseAudit?.n).toBe(0); // no reuse fired -- there was nothing to reuse + }); + + it("default (one_shot): a repeat trigger does not spend a fresh SLOP advisory call once this PR already had one, regardless of head SHA", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ risk: "low", rationale: "fine" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", slopGateMode: "advisory", slopAiAdvisory: true }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 93, title: "Slop one-shot PR", state: "open", user: { login: "contributor" }, head: { sha: "a93-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 93, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + // A slop advisory row already exists for an EARLIER head -- this PR already had its one-shot slop pass. + await putCachedAiSlopAdvisory(env, "JSONbored/gittensory", 93, "a93-v1", "seed-fp", { status: "ok", band: "low", finding: null, estimatedNeurons: 4 }); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/93/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/93")) return Response.json({ number: 93, title: "Slop one-shot PR", state: "open", user: { login: "contributor" }, head: { sha: "a93-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a93-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a93-v2/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/93/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-slop-push", repoFullName: "JSONbored/gittensory", prNumber: 93, installationId: 123 }); + + expect(aiCalls).toBe(0); // no fresh slop LLM call on the new head + const skipAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.ai_slop_one_shot_skip", "JSONbored/gittensory#93") + .first<{ outcome: string; detail: string }>(); + expect(skipAudit?.outcome).toBe("completed"); + expect(skipAudit?.detail).toContain("one-shot review cadence"); + }); + + it("default (one_shot): a NEWLY-linked issue still gets its own linked-issue-satisfaction pass even though the PR already has one for a DIFFERENT issue", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "looks done" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "advisory" }); + // The PR now cites issue #2 as its primary linked issue -- a prior pass exists only for issue #1, a + // DIFFERENT (now-superseded) issue, so issue #2 must still be treated as never-assessed. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 94, title: "Re-linked PR", state: "open", user: { login: "contributor" }, head: { sha: "a94" }, labels: [], body: "Closes #2" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 94, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 94, "a94-old", 1, "seed-fp", { status: "ok", result: { status: "addressed", rationale: "old issue was done", confidence: 0.9 }, estimatedNeurons: 4 }); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/94/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/94")) return Response.json({ number: 94, title: "Re-linked PR", state: "open", user: { login: "contributor" }, head: { sha: "a94" }, labels: [], body: "Closes #2", mergeable_state: "clean" }); + if (url.includes("/commits/a94/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a94/status")) return Response.json({ state: "success", statuses: [] }); + if (url.includes("/issues/2")) return Response.json({ number: 2, title: "Second issue", state: "open", labels: [], user: { login: "reporter" }, body: "Do the second thing." }); + if (url.includes("/issues/94/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-relinked", repoFullName: "JSONbored/gittensory", prNumber: 94, installationId: 123 }); + + expect(aiCalls).toBeGreaterThan(0); // issue #2 was never assessed before -- gets its own first pass + const skipAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") + .bind("github_app.linked_issue_satisfaction_one_shot_skip", "JSONbored/gittensory#94") + .first<{ n: number }>(); + expect(skipAudit?.n).toBe(0); // never skipped -- issue #2 genuinely had no prior pass + }); + + it("per-repo override (continuous via .gittensory.yml): a new push DOES spend a fresh main-review AI call, unlike the one_shot default", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Continuous re-review.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 95, title: "Continuous-mode PR", state: "open", user: { login: "contributor" }, head: { sha: "a95-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 95, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedAiReview(env, "JSONbored/gittensory", 95, "a95-v1", "block", { notes: "Original review.", reviewerCount: 1 }); + await markAiReviewPublished(env, "JSONbored/gittensory", 95, "a95-v1"); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/95/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/95")) return Response.json({ number: 95, title: "Continuous-mode PR", state: "open", user: { login: "contributor" }, head: { sha: "a95-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a95-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a95-v2/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/95/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "continuous-push", repoFullName: "JSONbored/gittensory", prNumber: 95, installationId: 123 }); + + expect(aiCalls).toBeGreaterThan(0); // continuous mode: the new head DOES buy a fresh AI review + const reuseAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") + .bind("github_app.ai_review_one_shot_reuse", "JSONbored/gittensory#95") + .first<{ n: number }>(); + expect(reuseAudit?.n).toBe(0); // one-shot reuse never engaged -- this repo opted out + }); + + it("fleet-wide env default (GITTENSORY_REVIEW_CONTINUOUS): applies when the repo has no yml override, but a repo override still wins over it", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_CONTINUOUS: "true", + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Fleet-wide continuous.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + // No per-repo cadence override -- inherits the fleet-wide GITTENSORY_REVIEW_CONTINUOUS default. + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 96, title: "Fleet-default PR", state: "open", user: { login: "contributor" }, head: { sha: "a96-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 96, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedAiReview(env, "JSONbored/gittensory", 96, "a96-v1", "block", { notes: "Original review.", reviewerCount: 1 }); + await markAiReviewPublished(env, "JSONbored/gittensory", 96, "a96-v1"); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/96/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/96")) return Response.json({ number: 96, title: "Fleet-default PR", state: "open", user: { login: "contributor" }, head: { sha: "a96-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a96-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a96-v2/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/96/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "fleet-continuous-push", repoFullName: "JSONbored/gittensory", prNumber: 96, installationId: 123 }); + + expect(aiCalls).toBeGreaterThan(0); // the fleet-wide env default applied since the repo set no override + }); + }); + it("#freeze-owner-exemption (incident, confirmed live on PR #3476): the repo owner's OWN held PR is never frozen -- a new push gets a fresh AI review", async () => { // The owner pushing a genuine fix to their OWN held PR must not keep replaying the ORIGINAL, now-stale // verdict pass after pass -- confirmed live via github_app.ai_review_frozen_reuse firing on every one of @@ -5894,6 +6146,10 @@ describe("queue processors", () => { AI_DAILY_NEURON_BUDGET: "100000", }); await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + // #one-shot-review-cadence: isolate this test to the owner-exemption-from-the-LABEL-freeze mechanism -- + // without this, a prior published review would ALSO be reused by the (correctly firing) one_shot default, + // for an unrelated reason, masking whether the label-freeze exemption itself actually works. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 79, title: "Owner's held PR", state: "open", user: { login: "JSONbored" }, head: { sha: "a79-v1" }, labels: [{ name: "manual-review" }], body: "Closes #1" }); await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 79, status: "complete", reviewsSyncedAt: new Date().toISOString() }); await putCachedAiReview(env, "JSONbored/gittensory", 79, "a79-v1", "block", { notes: "Original stale review.", reviewerCount: 1 }); @@ -5933,6 +6189,8 @@ describe("queue processors", () => { AI_DAILY_NEURON_BUDGET: "100000", }); await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + // #one-shot-review-cadence: isolate this test to the admin-exemption-from-the-LABEL-freeze mechanism. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 80, title: "Admin's held PR", state: "open", user: { login: "fleet-admin" }, head: { sha: "a80-v1" }, labels: [{ name: "manual-review" }], body: "Closes #1" }); await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 80, status: "complete", reviewsSyncedAt: new Date().toISOString() }); await putCachedAiReview(env, "JSONbored/gittensory", 80, "a80-v1", "block", { notes: "Original stale review.", reviewerCount: 1 }); @@ -5970,6 +6228,8 @@ describe("queue processors", () => { AI_DAILY_NEURON_BUDGET: "100000", }); await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + // #one-shot-review-cadence: isolate this test to the automation-bot-exemption-from-the-LABEL-freeze mechanism. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } }); await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 81, title: "Bot's held PR", state: "open", user: { login: "dependabot[bot]" }, head: { sha: "a81-v1" }, labels: [{ name: "manual-review" }], body: "Closes #1" }); await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 81, status: "complete", reviewsSyncedAt: new Date().toISOString() }); await putCachedAiReview(env, "JSONbored/gittensory", 81, "a81-v1", "block", { notes: "Original stale review.", reviewerCount: 1 }); @@ -6008,7 +6268,9 @@ describe("queue processors", () => { }); await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); // manualReviewLabel is config-as-code only (.gittensory.yml), not a DB-backed repository setting. - await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { manualReviewLabel: null } }); + // #one-shot-review-cadence: also opts into continuous here so this test stays isolated to the + // manualReviewLabel-disabled mechanism it's actually about. + await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { manualReviewLabel: null }, review: { auto_review: { cadence: "continuous" } } }); // The PR carries the literal "manual-review" text as a label, but with the mechanism disabled repo-wide // there is no configured label to match against — the freeze must never engage on text alone. await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 76, title: "Held PR, mechanism disabled", state: "open", user: { login: "contributor" }, head: { sha: "a76" }, labels: [{ name: "manual-review" }], body: "Closes #1" }); diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 0a04d33f39..10c7717602 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -1138,7 +1138,7 @@ describe("signal coverage edge cases", () => { collisions: buildCollisionReport(directRepo.fullName, [], [currentPr]), preflight: buildPreflightResult({ repoFullName: directRepo.fullName, title: "Fix isolated issue", body: "Fixes #99", linkedIssues: [99] }, directRepo, [], [currentPr]), settings: gateSettings, - review: { present: true, footerText: "Reviewed by the Acme maintainer bot.", note: "Run npm test before pushing.", fields: { relatedWork: false }, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, e2eTestDelivery: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { skipDrafts: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], skipDocsOnly: null, maxAddedLines: 0, maxFiles: 0, baseBranches: [], autoPauseAfterReviewedCommits: null }, labelingRules: [], aiModel: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null, ollamaModel: null, openaiModel: null, openaiCompatibleModel: null, anthropicModel: null }, visual: { productionUrl: null, preview: { urlTemplate: null }, routes: { paths: [], maxRoutes: null }, themes: [], gif: false, enabled: null, themeStorageKey: null, actionsFallback: false }, linkedIssueSatisfaction: null, sharedConfigSource: null }, + review: { present: true, footerText: "Reviewed by the Acme maintainer bot.", note: "Run npm test before pushing.", fields: { relatedWork: false }, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, e2eTestDelivery: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { skipDrafts: null, cadence: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], skipDocsOnly: null, maxAddedLines: 0, maxFiles: 0, baseBranches: [], autoPauseAfterReviewedCommits: null }, labelingRules: [], aiModel: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null, ollamaModel: null, openaiModel: null, openaiCompatibleModel: null, anthropicModel: null }, visual: { productionUrl: null, preview: { urlTemplate: null }, routes: { paths: [], maxRoutes: null }, themes: [], gif: false, enabled: null, themeStorageKey: null, actionsFallback: false }, linkedIssueSatisfaction: null, sharedConfigSource: null }, aiReview: { notes: "The change is focused.\n\n**Nits (2)**\n- Add a test for the edge case.\n- Keep the validator helper scoped." }, }); expect(customizedComment).toContain("Reviewed by the Acme maintainer bot."); // custom footer lead diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 4765b27099..538b5ebbc9 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 20fa12547178d4e155857d65d878ebff) +// Generated by Wrangler by running `wrangler types` (hash: 7eeadad15ce002765f06a319222e9f94) // Runtime types generated with workerd@1.20260701.1 2026-05-28 nodejs_compat interface __BaseEnv_Env { DB: D1Database; @@ -23,6 +23,7 @@ interface __BaseEnv_Env { GITTENSORY_REVIEW_SCREENSHOTS: "false"; GITTENSORY_REVIEW_GROUNDING: "false"; GITTENSORY_REVIEW_E2E_TESTS: "false"; + GITTENSORY_REVIEW_CONTINUOUS: "false"; GITTENSORY_REVIEW_REPUTATION: "false"; GITTENSORY_REVIEW_OPS: "false"; GITTENSORY_SWEEP_WATCHDOG: "false"; @@ -76,6 +77,7 @@ declare namespace NodeJS { | "GITTENSORY_PUBLIC_STATS" | "GITTENSORY_PUBLIC_STATS_REPOS" | "GITTENSORY_REVIEW_CONTENT_LANE" + | "GITTENSORY_REVIEW_CONTINUOUS" | "GITTENSORY_REVIEW_CULTURE_PROFILE" | "GITTENSORY_REVIEW_DRAFT" | "GITTENSORY_REVIEW_E2E_TESTS" diff --git a/wrangler.jsonc b/wrangler.jsonc index a53a529316..84ce6dff20 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -63,6 +63,13 @@ // E2E test coverage feature. Default OFF — flag-OFF the feature is never active for any repo regardless of // a per-repo features.e2eTests override. "GITTENSORY_REVIEW_E2E_TESTS": "false", + // #one-shot-review-cadence: fleet-wide default AI review re-trigger cadence, used only when a repo's + // .gittensory.yml review.auto_review.cadence is unset (a per-repo value always wins). Default OFF (false) + // = "one_shot": AI-generated content (main review, slop, linked-issue satisfaction) freezes after its + // first pass; only an explicit maintainer retrigger spends a fresh call. Truthy = "continuous": every + // push/CI-completion/sweep re-runs AI content generation, for operators who prefer that. The deterministic + // gate always re-evaluates regardless of this flag. + "GITTENSORY_REVIEW_CONTINUOUS": "false", // Convergence (reputation): factor the INTERNAL-only ported submitter-reputation signal into the AI-spend // gate — a new / burst / low-reputation submitter is downgraded to a deterministic-only review (AI neurons // skipped), and the per-(project, submitter) outcome is recorded after the gate decides. The reputation is From a3203e6c52280a43ecb893235d62668e896884bd Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:48:52 -0700 Subject: [PATCH 2/3] test(review): close linked-issue-skip coverage gap + mark unreachable headSha branches Add the missing positive-case test for the linked-issue-satisfaction one-shot skip actually firing (previously only the negative/never-skip case was covered), and mark the advisory.headSha ?? null fallback on the three new one-shot-cadence audit events as v8-ignored, mirroring the identical, already-established treatment of the same fallback shape elsewhere in maybePublishPrPublicSurface. --- src/queue/processors.ts | 9 +++++++++ test/unit/queue.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 32ad3aba8c..88a46baeea 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -9598,6 +9598,8 @@ async function maybePublishPrPublicSurface( targetKey: `${repoFullName}#${pr.number}`, outcome: "completed", detail: "one-shot review cadence: this PR already had its slop advisory pass; not spending a fresh call", + /* v8 ignore next -- reached only when a PRIOR slop pass already published, and an open PR does not + * lose its head SHA once set; the `?? null` is a type-level fallback for an unreachable branch. */ metadata: { repoFullName, headSha: advisory.headSha ?? null }, }).catch(() => undefined); } else { @@ -9642,6 +9644,9 @@ async function maybePublishPrPublicSurface( targetKey: `${repoFullName}#${pr.number}`, outcome: "completed", detail: "one-shot review cadence: this PR's linked issue already had its satisfaction pass; not spending a fresh call", + /* v8 ignore next -- reached only when a PRIOR satisfaction pass already published for this issue + * number, and an open PR does not lose its head SHA once set; the `?? null` is a type-level + * fallback for an unreachable branch. */ metadata: { repoFullName, headSha: advisory.headSha ?? null }, }).catch(() => undefined); } else { @@ -9982,6 +9987,10 @@ async function maybePublishPrPublicSurface( targetKey: `${repoFullName}#${pr.number}`, outcome: "completed", detail: "one-shot review cadence: reused the last published AI review instead of spending a fresh call", + /* v8 ignore next -- a truthy `oneShotPriorReview` means markAiReviewPublished previously stamped a row + * for a non-null head SHA, and an open PR does not lose its head SHA once set; the `?? null` is a + * type-level fallback for a practically-unreachable branch, mirroring the identical fallback on the + * frozen-reuse and paused-reuse audit events just above. */ metadata: { deliveryId: webhook.deliveryId, repoFullName, headSha: advisory.headSha ?? null }, }).catch(() => undefined); } diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index bb5bd5eceb..5554af35b7 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -6054,6 +6054,46 @@ describe("queue processors", () => { expect(skipAudit?.n).toBe(0); // never skipped -- issue #2 genuinely had no prior pass }); + it("default (one_shot): a repeat trigger does not spend a fresh linked-issue-satisfaction call once the SAME primary issue already has one", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "looks done" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "advisory" }); + // The PR's primary linked issue is #1 -- SAME issue the prior pass already assessed (at an earlier head). + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 97, title: "Same-issue PR", state: "open", user: { login: "contributor" }, head: { sha: "a97-v1" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 97, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 97, "a97-v1", 1, "seed-fp", { status: "ok", result: { status: "addressed", rationale: "already done", confidence: 0.9 }, estimatedNeurons: 4 }); + + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + // A genuinely NEW push -- a new head SHA, but the SAME primary linked issue (#1). + if (url.includes("/pulls/97/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]); + if (url.endsWith("/pulls/97")) return Response.json({ number: 97, title: "Same-issue PR", state: "open", user: { login: "contributor" }, head: { sha: "a97-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a97-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a97-v2/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/97/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-same-issue-push", repoFullName: "JSONbored/gittensory", prNumber: 97, installationId: 123 }); + + expect(aiCalls).toBe(0); // no fresh linked-issue-satisfaction call on the new head -- same issue already assessed + const skipAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.linked_issue_satisfaction_one_shot_skip", "JSONbored/gittensory#97") + .first<{ outcome: string; detail: string }>(); + expect(skipAudit?.outcome).toBe("completed"); + expect(skipAudit?.detail).toContain("one-shot review cadence"); + }); + it("per-repo override (continuous via .gittensory.yml): a new push DOES spend a fresh main-review AI call, unlike the one_shot default", async () => { let aiCalls = 0; const env = createTestEnv({ From a6b84b9d700ad4f6681817617e6f454a526ffc89 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 10 Jul 2026 03:15:42 -0700 Subject: [PATCH 3/3] test(review): cover the one-shot-cadence fail-safe catch arms codecov flagged codecov/patch flagged 6 lines at 81.82% patch coverage: the .catch() fallback bodies for the three new existence-check lookups and their paired audit-event writes never fired in the initial test suite (the happy path never rejects). Adds one combined read-failure + audit-write-failure regression per feature (slop, linked-issue satisfaction, main-review reuse), mirroring the exact two-part pattern already used for the pre-existing frozen-reuse fail-safe test. --- test/unit/queue.test.ts | 136 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 5554af35b7..cbf3a32fd8 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -6168,6 +6168,142 @@ describe("queue processors", () => { expect(aiCalls).toBeGreaterThan(0); // the fleet-wide env default applied since the repo set no override }); + + it("swallows a hasPublishedAiSlopAdvisory read failure and a slop one-shot-skip audit write failure without throwing", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ risk: "low", rationale: "fine" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", slopGateMode: "advisory", slopAiAdvisory: true }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 98, title: "Slop flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a98" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 98, 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: "fake-installation-token" }); + if (url.includes("/pulls/98/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/98")) return Response.json({ number: 98, title: "Slop flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a98" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a98/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a98/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/98/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + const readSpy = vi.spyOn(repositoriesModule, "hasPublishedAiSlopAdvisory").mockRejectedValueOnce(new Error("D1 read error")); + await expect( + processJob(env, { type: "agent-regate-pr", deliveryId: "slop-existence-read-fails", repoFullName: "JSONbored/gittensory", prNumber: 98, installationId: 123 }), + ).resolves.toBeUndefined(); + readSpy.mockRestore(); + expect(aiCalls).toBeGreaterThan(0); // fail-safe treats the read failure as "not skipped" -- a fresh slop call still runs + + await putCachedAiSlopAdvisory(env, "JSONbored/gittensory", 98, "a98", "seed-fp", { status: "ok", band: "low", finding: null, estimatedNeurons: 4 }); + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.ai_slop_one_shot_skip") throw new Error("audit DB down"); + await originalRecordAuditEvent(auditEnv, event); + }); + await expect( + processJob(env, { type: "agent-regate-pr", deliveryId: "slop-skip-audit-fails", repoFullName: "JSONbored/gittensory", prNumber: 98, installationId: 123 }), + ).resolves.toBeUndefined(); + auditSpy.mockRestore(); + }); + + it("swallows a hasPublishedLinkedIssueSatisfaction read failure and a linked-issue one-shot-skip audit write failure without throwing", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "looks done" }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "advisory" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 99, title: "Linked-issue flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a99" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 99, 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: "fake-installation-token" }); + if (url.includes("/pulls/99/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/99")) return Response.json({ number: 99, title: "Linked-issue flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a99" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a99/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a99/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/99/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 }); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + const readSpy = vi.spyOn(repositoriesModule, "hasPublishedLinkedIssueSatisfaction").mockRejectedValueOnce(new Error("D1 read error")); + await expect( + processJob(env, { type: "agent-regate-pr", deliveryId: "linked-issue-existence-read-fails", repoFullName: "JSONbored/gittensory", prNumber: 99, installationId: 123 }), + ).resolves.toBeUndefined(); + readSpy.mockRestore(); + expect(aiCalls).toBeGreaterThan(0); // fail-safe treats the read failure as "not skipped" -- a fresh assessment still runs + + await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 99, "a99", 1, "seed-fp", { status: "ok", result: { status: "addressed", rationale: "already done", confidence: 0.9 }, estimatedNeurons: 4 }); + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.linked_issue_satisfaction_one_shot_skip") throw new Error("audit DB down"); + await originalRecordAuditEvent(auditEnv, event); + }); + await expect( + processJob(env, { type: "agent-regate-pr", deliveryId: "linked-issue-skip-audit-fails", repoFullName: "JSONbored/gittensory", prNumber: 99, installationId: 123 }), + ).resolves.toBeUndefined(); + auditSpy.mockRestore(); + }); + + it("swallows a one-shot-reuse getLatestPublishedAiReview read failure and a one-shot-reuse audit write failure without throwing", async () => { + let aiCalls = 0; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Fresh.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + await seedRegateChurnRepo(env, { publicSurface: "comment_only" }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 100, title: "One-shot flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a100" }, labels: [], body: "Closes #1" }); + await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 100, status: "complete", reviewsSyncedAt: new Date().toISOString() }); + await putCachedAiReview(env, "JSONbored/gittensory", 100, "a100-old", "block", { notes: "Old.", reviewerCount: 1 }); + await markAiReviewPublished(env, "JSONbored/gittensory", 100, "a100-old"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" }); + if (url.includes("/pulls/100/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/100")) return Response.json({ number: 100, title: "One-shot flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a100" }, labels: [], body: "Closes #1", mergeable_state: "clean" }); + if (url.includes("/commits/a100/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/commits/a100/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/100/comments")) return method === "POST" || method === "PATCH" ? Response.json({ id: 1 }, { status: 201 }) : Response.json([]); + if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } }); + return Response.json({}); + }); + + const readSpy = vi.spyOn(repositoriesModule, "getLatestPublishedAiReview").mockRejectedValueOnce(new Error("D1 read error")); + await expect( + processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-reuse-read-fails", repoFullName: "JSONbored/gittensory", prNumber: 100, installationId: 123 }), + ).resolves.toBeUndefined(); + readSpy.mockRestore(); + expect(aiCalls).toBeGreaterThan(0); // fail-safe treats the read failure as "nothing to reuse" -- a fresh review still runs + + const originalRecordAuditEvent = repositoriesModule.recordAuditEvent; + const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => { + if (event.eventType === "github_app.ai_review_one_shot_reuse") throw new Error("audit DB down"); + await originalRecordAuditEvent(auditEnv, event); + }); + await expect( + processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-reuse-audit-fails", repoFullName: "JSONbored/gittensory", prNumber: 100, installationId: 123 }), + ).resolves.toBeUndefined(); + auditSpy.mockRestore(); + }); }); it("#freeze-owner-exemption (incident, confirmed live on PR #3476): the repo owner's OWN held PR is never frozen -- a new push gets a fresh AI review", async () => {