Skip to content
Closed
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
9 changes: 9 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ import {
buildReviewEnrichment,
isEnrichmentEnabled,
isReesGithubTokenForwardingEnabled,
resolveEnrichmentLinkedIssue,
} from "../review/enrichment-wire";
import { captureReviewFailure } from "../selfhost/sentry";
import { evaluateWithSurfaceLane } from "../review/content-lane-wire";
Expand Down Expand Up @@ -3641,6 +3642,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 @@ -3809,6 +3811,11 @@ export async function runAiReviewForAdvisory(
// its public-safe brief splices into the prompt next to grounding + RAG. Flag-OFF (default) → no call, no branch,
// byte-identical prompt. Fully fail-safe (any timeout/error/empty → undefined → review proceeds).
const enrichmentDiff = buildAiReviewDiff(files);
const enrichmentLinkedIssue = await resolveEnrichmentLinkedIssue(
env,
args.repoFullName,
args.pr.linkedIssues ?? [],
);
const enrichment =
isEnrichmentEnabled(env) && convergedRepoAllowed
? await buildReviewEnrichment(env, {
Expand All @@ -3817,7 +3824,9 @@ export async function runAiReviewForAdvisory(
headSha: args.advisory.headSha,
baseSha: args.pr.baseSha ?? null,
title: args.pr.title,
body: args.pr.body ?? undefined,
author: args.author,
linkedIssue: enrichmentLinkedIssue,
githubToken: isReesGithubTokenForwardingEnabled(env)
? await resolveReviewEnrichmentGithubToken(
env,
Expand Down
29 changes: 29 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 { getIssue } from "../db/repositories";
import { sanitizePublicComment } from "../queue-intelligence";
import { neutralizePromptInjection } from "./prompt-injection";
import type { PullRequestFileRecord } from "../types";
Expand Down Expand Up @@ -135,18 +136,44 @@ function headShaPrefix(headSha: string | null | undefined): string | undefined {
return text ? text.slice(0, 12) : undefined;
}

export interface EnrichmentLinkedIssue {
number: number;
title?: string;
body?: string;
}

interface EnrichmentInput {
repoFullName: string;
prNumber: number;
headSha: string | null;
baseSha?: string | null;
title?: string | undefined;
body?: string | undefined;
author?: string | null | undefined;
linkedIssue?: EnrichmentLinkedIssue | undefined;
githubToken?: string | undefined;
files: PullRequestFileRecord[];
diff: string;
}

/** Resolve the PR's primary linked issue into the compact REES envelope (#1478). Fail-safe: returns
* `{ number }` when the issue row is missing so the history analyzer can still key on the number. */
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 Expand Up @@ -232,7 +259,9 @@ export async function buildReviewEnrichment(
headSha: input.headSha,
baseSha: input.baseSha ?? null,
title: input.title,
...(input.body ? { body: input.body } : {}),
author: input.author ?? undefined,
...(input.linkedIssue ? { linkedIssue: input.linkedIssue } : {}),
...(input.githubToken ? { githubToken: input.githubToken } : {}),
files: input.files.map((file) => ({
path: file.path,
Expand Down
52 changes: 52 additions & 0 deletions test/unit/enrichment-wire.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import {
isEnrichmentEnabled,
buildReviewEnrichment,
isReesGithubTokenForwardingEnabled,
resolveEnrichmentLinkedIssue,
resolveReesAnalyzers,
resolveReesAnalyzerBudgetMs,
resolveReesProfile,
resolveReesTransportTimeoutMs,
} from "../../src/review/enrichment-wire";
import { upsertIssueFromGitHub } from "../../src/db/repositories";
import { createTestEnv } from "../helpers/d1";

const env = (o: Record<string, string>) => o as unknown as Env;
const input = {
Expand Down Expand Up @@ -83,7 +86,9 @@ describe("buildReviewEnrichment", () => {
{
...input,
baseSha: "baseabc",
body: "Fixes #12",
author: "alice",
linkedIssue: { number: 12, title: "Add caching", body: "Cache registry sync." },
githubToken: "gh-read-token",
},
);
Expand All @@ -106,6 +111,12 @@ describe("buildReviewEnrichment", () => {
const body = JSON.parse(calls[0]!.init.body as string);
expect(body.repoFullName).toBe("o/r");
expect(body.baseSha).toBe("baseabc");
expect(body.body).toBe("Fixes #12");
expect(body.linkedIssue).toEqual({
number: 12,
title: "Add caching",
body: "Cache registry sync.",
});
expect(body.author).toBe("alice");
expect(body.githubToken).toBe("gh-read-token");
expect(body.analyzers).toBeUndefined();
Expand Down Expand Up @@ -561,6 +572,47 @@ describe("resolveReesProfile", () => {
});
});

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

it("returns number-only envelope when the issue row is missing", async () => {
const env = createTestEnv();
await expect(
resolveEnrichmentLinkedIssue(env, "o/r", [42]),
).resolves.toEqual({ number: 42 });
});

it("returns title/body from the cached issue row", async () => {
const env = createTestEnv();
await upsertIssueFromGitHub(env, "o/r", {
number: 12,
title: "Add caching",
body: "Cache registry sync.",
state: "open",
user: { login: "alice" },
labels: [],
html_url: "https://github.com/o/r/issues/12",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
});
await expect(
resolveEnrichmentLinkedIssue(env, "o/r", [12]),
).resolves.toEqual({
number: 12,
title: "Add caching",
body: "Cache registry sync.",
});
});
});

describe("REES timeout budget helpers", () => {
it("keeps analyzer execution below the HTTP transport timeout", () => {
expect(resolveReesTransportTimeoutMs(undefined)).toBe(8000);
Expand Down
26 changes: 24 additions & 2 deletions test/unit/enrichment-wiring.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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";

Expand Down Expand Up @@ -85,13 +85,26 @@ 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: "Add feature",
body: "Implement the requested feature.",
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;
body?: {
analyzers?: string[];
baseSha?: string | null;
author?: string;
body?: string;
linkedIssue?: { number: number; title?: string; body?: string };
githubToken?: string;
};
} = {};
Expand All @@ -105,6 +118,8 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
analyzers?: string[];
baseSha?: string | null;
author?: string;
body?: string;
linkedIssue?: { number: number; title?: string; body?: string };
githubToken?: string;
};
return new Response(
Expand All @@ -124,8 +139,9 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
pr: {
number: 7,
title: "Add a feature",
body: "Implements the thing.",
body: "Implements the thing. Fixes #42",
baseSha: "base7",
linkedIssues: [42],
},
author: "alice",
confirmedContributor: true,
Expand All @@ -141,6 +157,12 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE
]);
expect(reesRequest.body?.baseSha).toBe("base7");
expect(reesRequest.body?.author).toBe("alice");
expect(reesRequest.body?.body).toBe("Implements the thing. Fixes #42");
expect(reesRequest.body?.linkedIssue).toEqual({
number: 42,
title: "Add feature",
body: "Implement the requested feature.",
});
expect(reesRequest.body?.githubToken).toBe("public-read-token");
// 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.
Expand Down
Loading