Skip to content

Commit 3a5f83f

Browse files
Capture complete local PR diff evidence
1 parent e205f25 commit 3a5f83f

2 files changed

Lines changed: 107 additions & 7 deletions

File tree

src/adapter.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2884,6 +2884,18 @@ async function prepareReviewContext(
28842884
}
28852885
const files = await githubRequestAllPages(`https://api.github.com/repos/${repo}/pulls/${prNumber}/files`, token);
28862886

2887+
const fetchBase = runCommand([config.hostGitBin, "fetch", "--depth", "1", "origin", liveBase], workspace, env, 180);
2888+
writeJsonAtomic(join(attemptDir, "fetch-pr-base.json"), redactedCommandResult(fetchBase));
2889+
if (fetchBase.returncode !== 0) {
2890+
return {
2891+
kind: "pull_request",
2892+
pr_number: prNumber,
2893+
fetch_error: fetchBase.stderr,
2894+
metadata: summarizePr(pr),
2895+
files: summarizePrFiles(files, new Map()),
2896+
};
2897+
}
2898+
28872899
const fetch = runCommand([config.hostGitBin, "fetch", "--depth", "1", "origin", `pull/${prNumber}/head`], workspace, env, 180);
28882900
writeJsonAtomic(join(attemptDir, "fetch-pr.json"), redactedCommandResult(fetch));
28892901
if (fetch.returncode !== 0) {
@@ -2892,7 +2904,7 @@ async function prepareReviewContext(
28922904
pr_number: prNumber,
28932905
fetch_error: fetch.stderr,
28942906
metadata: summarizePr(pr),
2895-
files: summarizePrFiles(files),
2907+
files: summarizePrFiles(files, new Map()),
28962908
};
28972909
}
28982910

@@ -2913,11 +2925,37 @@ async function prepareReviewContext(
29132925
spawn_error: [head.spawn_error, status.spawn_error].filter(Boolean).join("\n"),
29142926
}));
29152927

2928+
const localPatches = new Map<string, CommandResult>();
2929+
const localPatchEvidence: JsonObject[] = [];
2930+
for (const file of files) {
2931+
const filename = String(file.filename || "");
2932+
if (!filename) continue;
2933+
const diffPaths = [...new Set([String(file.previous_filename || ""), filename].filter(Boolean))];
2934+
const diff = runCommand(
2935+
[config.hostGitBin, "diff", "--no-ext-diff", "--unified=3", liveBase, liveHead, "--", ...diffPaths],
2936+
workspace,
2937+
env,
2938+
180,
2939+
);
2940+
localPatches.set(filename, diff);
2941+
localPatchEvidence.push({
2942+
filename,
2943+
paths: diffPaths,
2944+
returncode: diff.returncode,
2945+
patch_bytes: Buffer.byteLength(diff.stdout, "utf8"),
2946+
patch_sha256: diff.returncode === 0 ? sha256(diff.stdout) : undefined,
2947+
stderr: redactTokenish(diff.stderr),
2948+
timed_out: diff.timed_out,
2949+
spawn_error: diff.spawn_error,
2950+
});
2951+
}
2952+
writeJsonAtomic(join(attemptDir, "local-pr-diff-evidence.json"), localPatchEvidence);
2953+
29162954
return {
29172955
kind: "pull_request",
29182956
pr_number: prNumber,
29192957
metadata: summarizePr(pr),
2920-
files: summarizePrFiles(files),
2958+
files: summarizePrFiles(files, localPatches),
29212959
checkout: {
29222960
fetch_returncode: fetch.returncode,
29232961
checkout_returncode: checkout.returncode,
@@ -2944,19 +2982,30 @@ function summarizePr(pr: JsonObject): JsonObject {
29442982
};
29452983
}
29462984

2947-
function summarizePrFiles(files: JsonObject[]): JsonObject[] {
2985+
export function summarizePrFiles(files: JsonObject[], localPatches?: ReadonlyMap<string, CommandResult>): JsonObject[] {
29482986
return (files || []).map((item) => {
2949-
const patch = String(item.patch || "");
2987+
const filename = String(item.filename || "");
2988+
const localPatch = localPatches?.get(filename);
2989+
const localCaptureRequired = localPatches !== undefined;
2990+
const localCaptureSucceeded = localPatch?.returncode === 0;
2991+
const patch = localCaptureSucceeded ? localPatch.stdout : String(item.patch || "");
29502992
const patchTruncated = patchEvidenceIncomplete(patch, Number(item.additions || 0), Number(item.deletions || 0));
2993+
const binaryPatch = patch.includes("GIT binary patch") || patch.includes("Binary files ");
29512994
return {
2952-
filename: item.filename,
2995+
filename,
29532996
status: item.status,
29542997
additions: item.additions,
29552998
deletions: item.deletions,
29562999
changes: item.changes,
29573000
sha: item.sha,
2958-
patch: patch.slice(0, 12000),
2959-
patch_truncated: patch.length > 12000 || patchTruncated,
3001+
patch: localCaptureSucceeded ? patch : patch.slice(0, 12000),
3002+
patch_source: localCaptureSucceeded ? "local_exact_base_head" : "github_files_api",
3003+
patch_sha256: patch ? sha256(patch) : undefined,
3004+
patch_truncated: !patch.trim()
3005+
|| binaryPatch
3006+
|| patchTruncated
3007+
|| (!localCaptureSucceeded && patch.length > 12000)
3008+
|| (localCaptureRequired && !localCaptureSucceeded),
29603009
};
29613010
});
29623011
}

tests/webhook-adapter.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
runTask,
4242
sanitizedRuntimeEnvironment,
4343
sessionBrief,
44+
summarizePrFiles,
4445
trustedValidationFindings,
4546
waitForExpectedPullRevision,
4647
withEphemeralCodexCredential,
@@ -1997,6 +1998,56 @@ test("detects a patch already truncated by the GitHub files API", () => {
19971998
assert.equal(patchEvidenceIncomplete(completePatch.slice(0, -9), 2, 2), true);
19981999
});
19992000

2001+
test("uses complete exact-base/head patches instead of truncated GitHub API patches", () => {
2002+
const completePatch = "diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1,2 +1,2 @@\n-old one\n-old two\n+new one\n+new two";
2003+
const files = [{
2004+
filename: "src/a.ts",
2005+
status: "modified",
2006+
additions: 2,
2007+
deletions: 2,
2008+
changes: 4,
2009+
patch: completePatch.slice(0, -9),
2010+
}];
2011+
const localPatches = new Map([[
2012+
"src/a.ts",
2013+
{
2014+
args: ["git", "diff"],
2015+
returncode: 0,
2016+
stdout: completePatch,
2017+
stderr: "",
2018+
signal: null,
2019+
timed_out: false,
2020+
spawn_error: "",
2021+
},
2022+
]]);
2023+
2024+
const [summary] = summarizePrFiles(files, localPatches);
2025+
assert.equal(summary.patch, completePatch);
2026+
assert.equal(summary.patch_source, "local_exact_base_head");
2027+
assert.equal(summary.patch_truncated, false);
2028+
});
2029+
2030+
test("fails closed when exact-base/head patch capture fails", () => {
2031+
const apiPatch = "@@ -1 +1 @@\n-old\n+new";
2032+
const files = [{filename: "src/a.ts", status: "modified", additions: 1, deletions: 1, changes: 2, patch: apiPatch}];
2033+
const localPatches = new Map([[
2034+
"src/a.ts",
2035+
{
2036+
args: ["git", "diff"],
2037+
returncode: 128,
2038+
stdout: "",
2039+
stderr: "bad revision",
2040+
signal: null,
2041+
timed_out: false,
2042+
spawn_error: "",
2043+
},
2044+
]]);
2045+
2046+
const [summary] = summarizePrFiles(files, localPatches);
2047+
assert.equal(summary.patch_source, "github_files_api");
2048+
assert.equal(summary.patch_truncated, true);
2049+
});
2050+
20002051
test("rejects syntactically valid supporting paths that are missing from the checkout", () => {
20012052
const root = tempStateDir();
20022053
const task = reviewTask();

0 commit comments

Comments
 (0)