From 0d892610c1c7c6197b015ec6fdd864c73b69d194 Mon Sep 17 00:00:00 2001 From: shin-core <153108882+shin-core@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:59:00 +0900 Subject: [PATCH] fix(review): reserve header length in buildUnifiedReviewDiff's no-patch branch The patched-file branch of buildUnifiedReviewDiff already reserves the header's length against the remaining budget before appending, but the no-patch branch (binary/too-large files) appended header + suffix unconditionally. A long enough file.path made that header overflow the caller's budget, breaking the same bounded-output invariant the patched branch upholds. Mirror the patched branch's reservation: when header.length + suffix.length would exceed the remaining budget, fall through to the existing truncation-notice-and-break path instead of silently overflowing. The common case (a short path that fits comfortably) renders byte-for-byte as before. Closes #10327 --- src/review/review-diff.ts | 10 +++++++++- test/unit/review-diff.test.ts | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/review/review-diff.ts b/src/review/review-diff.ts index bda991a55..011c3f083 100644 --- a/src/review/review-diff.ts +++ b/src/review/review-diff.ts @@ -185,7 +185,15 @@ export function buildUnifiedReviewDiff(files: ReviewDiffFile[], budget: number = break; } if (!file.patch) { - diff += `${header}(no inline patch — binary or too large)\n\n`; + const suffix = `(no inline patch — binary or too large)\n\n`; + // Mirror the patched branch's `header.length + body.length + 2 > remaining` reservation: a long + // enough `file.path` can make the header+suffix overflow `budget`, so fall through to the same + // truncation-notice-and-break path the `remaining < 240` case uses rather than silently overflowing (#10327). + if (header.length + suffix.length > remaining) { + diff += truncationNotice; + break; + } + diff += `${header}${suffix}`; continue; } let body = file.patch; diff --git a/test/unit/review-diff.test.ts b/test/unit/review-diff.test.ts index 21cb9fc02..981ae4b49 100644 --- a/test/unit/review-diff.test.ts +++ b/test/unit/review-diff.test.ts @@ -168,6 +168,26 @@ describe("buildUnifiedReviewDiff — the #1528 fix: never silently drop the file }); }); +describe("buildUnifiedReviewDiff — the #10327 fix: a patch-less file's header+suffix respects the remaining budget", () => { + it("falls through to the truncation-notice-and-break path when a long path makes header+suffix overflow, staying within budget", () => { + // remaining >= 240 (past the `remaining < 240` break), but a ~250-char path makes header+suffix (~310) exceed it, + // so the patch-less branch must break with the truncation notice rather than silently overflow `budget` (#10327). + const longPath = "a".repeat(250); + const budget = 250; + const diff = buildUnifiedReviewDiff([{ path: longPath, patch: undefined, status: "added", additions: 0, deletions: 0 }], budget); + expect(diff.length).toBeLessThanOrEqual(budget); // the invariant the patched branch already holds + expect(diff).toContain("…diff truncated"); // the drop is announced, not silent + expect(diff).not.toContain("no inline patch"); // the overflowing file was not appended + }); + + it("renders a short-path patch-less file unchanged when it comfortably fits (the header+suffix fits, so no break)", () => { + const diff = buildUnifiedReviewDiff([{ path: "logo.png", patch: undefined, status: "added", additions: 0, deletions: 0 }]); + expect(diff).toContain("### logo.png (added) +0/-0"); + expect(diff).toContain("(no inline patch — binary or too large)"); + expect(diff).not.toContain("…diff truncated"); // it fit, so no truncation + }); +}); + describe("keepHighSignalHunks — non-positive budget guard (#5849)", () => { it("returns only the truncation marker when the budget is zero", () => { expect(keepHighSignalHunks("@@ a\n+x\n+y", 0)).toBe("… (this file's diff truncated)");