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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ import {
buildReviewEnrichment,
isEnrichmentEnabled,
isReesGithubTokenForwardingEnabled,
resolveEnrichmentLinkedIssue,
resolveEnrichmentLinkedIssueNumbers,
} from "../review/enrichment-wire";
import { captureReviewFailure } from "../selfhost/sentry";
import { evaluateWithSurfaceLane } from "../review/content-lane-wire";
Expand Down Expand Up @@ -3646,6 +3648,7 @@ export async function runAiReviewForAdvisory(
title: string;
body?: string | null | undefined;
baseSha?: string | null | undefined;
linkedIssues?: number[] | undefined;
};
author: string | null;
confirmedContributor: boolean;
Expand Down Expand Up @@ -3827,6 +3830,14 @@ export async function runAiReviewForAdvisory(
title: args.pr.title,
body: args.pr.body ?? undefined,
author: args.author,
linkedIssue: await resolveEnrichmentLinkedIssue(
env,
args.repoFullName,
resolveEnrichmentLinkedIssueNumbers(
args.pr.linkedIssues,
args.pr.body,
),
),
githubToken: isReesGithubTokenForwardingEnabled(env)
? await resolveReviewEnrichmentGithubToken(
env,
Expand Down
28 changes: 28 additions & 0 deletions src/review/enrichment-wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Single env switch: GITTENSORY_REVIEW_ENRICHMENT (+ REES_URL must be set, so the hosted Worker — which sets neither
// — is unaffected). Default OFF → gathers nothing, prompt byte-identical. FULLY FAIL-SAFE: any timeout / non-200 /
// network / parse error, or an empty brief, returns undefined and the review proceeds on diff + grounding + RAG.
import { extractLinkedIssueNumbers, getIssue } from "../db/repositories";
import { sanitizePublicComment } from "../queue-intelligence";
import { neutralizePromptInjection } from "./prompt-injection";
import type { PullRequestFileRecord } from "../types";
Expand Down Expand Up @@ -156,6 +157,33 @@ interface EnrichmentInput {
diff: string;
}

/** Prefer explicit linkedIssues; fall back to Fixes #N parsing from the PR body. */
export function resolveEnrichmentLinkedIssueNumbers(
linkedIssues: number[] | undefined,
body: string | null | undefined,
): number[] {
const explicit = (linkedIssues ?? []).filter((candidate) => Number.isInteger(candidate) && candidate > 0);
if (explicit.length > 0) return explicit;
return extractLinkedIssueNumbers(body ?? "");
}

/** Resolve the PR's primary linked issue into the compact REES envelope (#1478). */
export async function resolveEnrichmentLinkedIssue(
env: Env,
repoFullName: string,
linkedIssues: number[],
): Promise<EnrichmentLinkedIssue | undefined> {
const number = linkedIssues.find((candidate) => Number.isInteger(candidate) && candidate > 0);
if (!number) return undefined;
const issue = await getIssue(env, repoFullName, number).catch(() => null);
if (!issue) return { number };
return {
number: issue.number,
...(issue.title ? { title: issue.title } : {}),
...(issue.body ? { body: issue.body } : {}),
};
}

/** Optional comma-list of REES analyzers. Unset/"all" omits the field so REES runs its full registry.
* An explicit typo-only list fails closed by sending [] rather than expanding to every analyzer. */
export function resolveReesAnalyzers(env: Env): string[] | undefined {
Expand Down
103 changes: 103 additions & 0 deletions test/unit/enrichment-wire.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
resolveReesAnalyzerBudgetMs,
resolveReesProfile,
resolveReesTransportTimeoutMs,
resolveEnrichmentLinkedIssue,
resolveEnrichmentLinkedIssueNumbers,
} from "../../src/review/enrichment-wire";
import { createTestEnv } from "../helpers/d1";
import { upsertIssueFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories";

const env = (o: Record<string, string>) => o as unknown as Env;
const input = {
Expand Down Expand Up @@ -130,6 +134,27 @@ describe("buildReviewEnrichment", () => {
]);
});

it("includes linkedIssue in the REES POST when provided", async () => {
const calls: RequestInit[] = [];
globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => {
calls.push(init);
return {
ok: true,
json: async () => ({ promptSection: "brief" }),
} as Response;
}) as unknown as typeof fetch;
await buildReviewEnrichment(env({ REES_URL: "https://r" }), {
...input,
linkedIssue: { number: 42, title: "Fix cache", body: "Details here." },
});
const body = JSON.parse(calls[0]!.body as string);
expect(body.linkedIssue).toEqual({
number: 42,
title: "Fix cache",
body: "Details here.",
});
});

it("sends an analyzer budget below the transport timeout and accepts partial degraded briefs", async () => {
let body: { budget?: { timeoutMs?: number; maxBriefChars?: number } } | undefined;
globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => {
Expand Down Expand Up @@ -545,6 +570,84 @@ describe("resolveReesAnalyzers", () => {
});
});

describe("resolveEnrichmentLinkedIssueNumbers", () => {
it("prefers explicit linkedIssues over body parsing", () => {
expect(resolveEnrichmentLinkedIssueNumbers([7], "Fixes #42")).toEqual([7]);
});

it("parses Fixes #N from the PR body when linkedIssues is empty", () => {
expect(resolveEnrichmentLinkedIssueNumbers([], "Fixes #42\nCloses #99")).toEqual([42, 99]);
expect(resolveEnrichmentLinkedIssueNumbers(undefined, "Resolves #3")).toEqual([3]);
});

it("returns an empty list when neither source yields issue numbers", () => {
expect(resolveEnrichmentLinkedIssueNumbers([], "no issue refs")).toEqual([]);
expect(resolveEnrichmentLinkedIssueNumbers(undefined, undefined)).toEqual([]);
});
});

describe("resolveEnrichmentLinkedIssue", () => {
it("returns undefined when no linked issue numbers are provided", async () => {
const env = createTestEnv({});
expect(await resolveEnrichmentLinkedIssue(env, "o/r", [])).toBeUndefined();
expect(await resolveEnrichmentLinkedIssue(env, "o/r", [0, -1])).toBeUndefined();
});

it("returns the compact envelope from the local issue cache", async () => {
const env = createTestEnv({});
await upsertRepositoryFromGitHub(
env,
{ name: "r", full_name: "o/r", private: false, owner: { login: "o" } },
1,
);
await upsertIssueFromGitHub(env, "o/r", {
number: 42,
title: "Fix cache race",
body: "Repro steps inside.",
state: "open",
user: { login: "reporter" },
labels: [],
html_url: "https://github.com/o/r/issues/42",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
});
expect(await resolveEnrichmentLinkedIssue(env, "o/r", [42])).toEqual({
number: 42,
title: "Fix cache race",
body: "Repro steps inside.",
});
});

it("falls back to number-only when the issue is not cached locally", async () => {
const env = createTestEnv({});
expect(await resolveEnrichmentLinkedIssue(env, "o/r", [99])).toEqual({ number: 99 });
});

it("uses the first positive linked issue number", async () => {
const env = createTestEnv({});
await upsertRepositoryFromGitHub(
env,
{ name: "r", full_name: "o/r", private: false, owner: { login: "o" } },
1,
);
await upsertIssueFromGitHub(env, "o/r", {
number: 7,
title: "Primary",
body: "",
state: "open",
user: { login: "reporter" },
labels: [],
html_url: "https://github.com/o/r/issues/7",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
});
expect(await resolveEnrichmentLinkedIssue(env, "o/r", [0, 7, 8])).toEqual({
number: 7,
title: "Primary",
});
});
});

describe("resolveReesProfile", () => {
it("returns undefined for unset profiles", () => {
expect(resolveReesProfile(env({}))).toBeUndefined();
Expand Down
95 changes: 92 additions & 3 deletions test/unit/enrichment-wiring.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import { runAiReviewForAdvisory } from "../../src/queue/processors";
import { upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { upsertRepositoryFromGitHub, upsertIssueFromGitHub } from "../../src/db/repositories";
import type { Advisory, RepositorySettings } from "../../src/types";
import { createTestEnv } from "../helpers/d1";
import * as enrichmentWire from "../../src/review/enrichment-wire";

const notesJson = JSON.stringify({
assessment: "Looks fine.",
Expand Down Expand Up @@ -85,6 +86,17 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
REES_ANALYZERS: "secret,actionPin,redos",
});
await seedRepoFile(env, "acme/widgets");
await upsertIssueFromGitHub(env, "acme/widgets", {
number: 42,
title: "Linked bug",
body: "Issue context for history analyzer.",
state: "open",
user: { login: "reporter" },
labels: [],
html_url: "https://github.com/acme/widgets/issues/42",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
});
const reesRequest: {
url?: string;
auth?: string | null;
Expand All @@ -94,6 +106,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
author?: string;
body?: string;
githubToken?: string;
linkedIssue?: { number: number; title?: string; body?: string };
};
} = {};
const fetchSpy = vi
Expand All @@ -108,6 +121,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
author?: string;
body?: string;
githubToken?: string;
linkedIssue?: { number: number; title?: string; body?: string };
};
return new Response(
JSON.stringify({
Expand All @@ -128,6 +142,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
title: "Add a feature",
body: "Implements the thing.",
baseSha: "base7",
linkedIssues: [42],
},
author: "alice",
confirmedContributor: true,
Expand All @@ -145,6 +160,11 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
expect(reesRequest.body?.author).toBe("alice");
expect(reesRequest.body?.body).toBe("Implements the thing.");
expect(reesRequest.body?.githubToken).toBe("public-read-token");
expect(reesRequest.body?.linkedIssue).toEqual({
number: 42,
title: "Linked bug",
body: "Issue context for history analyzer.",
});
// The brief's content flows into the user prompt, but the system prompt carries our FIXED
// enrichment suffix — the REES-supplied systemSuffix is untrusted and is never spliced in.
expect(seenUser[0] ?? "").toContain("## EXTERNAL REVIEW BRIEF");
Expand All @@ -155,7 +175,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
}
});

it("FLAG-OFF (default): the REES is never called", async () => {
it("FLAG-OFF (default): the REES is never called and linked issues are not resolved", async () => {
const run = vi.fn(async () => ({ response: notesJson }));
const env = createTestEnv({
AI: { run } as unknown as Ai,
Expand All @@ -164,6 +184,10 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
AI_DAILY_NEURON_BUDGET: "100000",
});
await seedRepoFile(env, "acme/off");
const linkedIssueSpy = vi.spyOn(
enrichmentWire,
"resolveEnrichmentLinkedIssue",
);
let reesCalled = false;
const fetchSpy = vi
.spyOn(globalThis, "fetch")
Expand All @@ -175,13 +199,15 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
await runAiReviewForAdvisory(env, {
settings: { aiReviewMode: "advisory" } as RepositorySettings,
repoFullName: "acme/off",
pr: { number: 7, title: "t", body: "b" },
pr: { number: 7, title: "t", body: "Fixes #42", linkedIssues: [42] },
author: "alice",
confirmedContributor: true,
advisory: adv("acme/off"),
});
expect(reesCalled).toBe(false);
expect(linkedIssueSpy).not.toHaveBeenCalled();
} finally {
linkedIssueSpy.mockRestore();
fetchSpy.mockRestore();
}
});
Expand Down Expand Up @@ -232,4 +258,67 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
fetchSpy.mockRestore();
}
});

it("derives linkedIssue from Fixes #N in the PR body when linkedIssues is empty", async () => {
const run = vi.fn(async () => ({ response: notesJson }));
const env = createTestEnv({
AI: { run } as unknown as Ai,
AI_SUMMARIES_ENABLED: "true",
AI_PUBLIC_COMMENTS_ENABLED: "true",
AI_DAILY_NEURON_BUDGET: "100000",
});
Object.assign(env, {
GITTENSORY_REVIEW_ENRICHMENT: "true",
REES_URL: "https://rees.example",
REES_SHARED_SECRET: "sek",
});
await seedRepoFile(env, "acme/widgets");
await upsertIssueFromGitHub(env, "acme/widgets", {
number: 55,
title: "Body-linked bug",
body: "Parsed from PR description.",
state: "open",
user: { login: "reporter" },
labels: [],
html_url: "https://github.com/acme/widgets/issues/55",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
});
let reesBody: { linkedIssue?: { number: number; title?: string; body?: string } } | undefined;
const fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (url, init) => {
if (String(url).includes("/v1/enrich")) {
reesBody = JSON.parse(String(init?.body ?? "{}")) as {
linkedIssue?: { number: number; title?: string; body?: string };
};
return new Response(JSON.stringify({ promptSection: "brief" }), {
status: 200,
});
}
return new Response("nope", { status: 404 });
});
try {
await runAiReviewForAdvisory(env, {
settings: { aiReviewMode: "advisory" } as RepositorySettings,
repoFullName: "acme/widgets",
pr: {
number: 7,
title: "Fix the bug",
body: "Fixes #55",
linkedIssues: [],
},
author: "alice",
confirmedContributor: true,
advisory: adv("acme/widgets"),
});
expect(reesBody?.linkedIssue).toEqual({
number: 55,
title: "Body-linked bug",
body: "Parsed from PR description.",
});
} finally {
fetchSpy.mockRestore();
}
});
});
Loading