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
8 changes: 8 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,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 @@ -3646,6 +3647,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 @@ -3817,6 +3819,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 @@ -3827,6 +3834,7 @@ export async function runAiReviewForAdvisory(
title: args.pr.title,
body: args.pr.body ?? undefined,
author: args.author,
linkedIssue: enrichmentLinkedIssue,
githubToken: isReesGithubTokenForwardingEnabled(env)
? await resolveReviewEnrichmentGithubToken(
env,
Expand Down
18 changes: 18 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 @@ -155,6 +156,23 @@ interface EnrichmentInput {
diff: string;
}

/** 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
86 changes: 86 additions & 0 deletions test/unit/enrichment-wire.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
resolveReesAnalyzerBudgetMs,
resolveReesProfile,
resolveReesTransportTimeoutMs,
resolveEnrichmentLinkedIssue,
} 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 +133,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 @@ -538,6 +562,68 @@ describe("resolveReesAnalyzers", () => {
});
});

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
21 changes: 20 additions & 1 deletion 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,6 +85,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 +105,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 +120,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 +141,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 +159,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 Down
Loading