Files
backnotprop__plannotator/packages/shared/diff-paths.test.ts
Michael Ramos 75e8b78cc7 fix(review): stop stubbing files whose worktree content the size probe cannot find (#1220)
The oversized preflight in buildBoundedTrackedDiff mapped every object the
cat-file batch could not size to infinity. `missing` is routine: for
tree-vs-worktree diffs git hashes the WORKING-TREE content of any path pulled
into rename/copy detection and prints that hash in --raw output without ever
writing the blob, and partial clones report it for unfetched blobs. Those
files were excluded by pathspec and replaced with a contents-free binary stub,
so a renamed-and-edited file rendered as a silently empty card and
/api/file-content refused it as binary.

Missing now means unknown, not oversized, and an unreadable new side is bounded
by the working-tree file's stat size instead. Every rendered diff stays bounded
git-side by core.bigFileThreshold (#1205), genuinely oversized files still stub
via real probe sizes plus the stat door, and a blob git truly cannot read now
fails loudly through assertGitSuccess instead of blanking a file.

Client side, a chunk with a binary marker and no hunks now renders an explicit
placeholder in both the all-files view and the single-file viewer, so an empty
card can never again pass for "no changes here".

AI-assisted.
2026-08-06 00:47:19 -07:00

125 lines
4.2 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
isContentlessBinaryPatch,
parseDiffFilePathLines,
parsePatchPathToken,
unquoteGitPath,
} from "./diff-paths";
describe("isContentlessBinaryPatch", () => {
test("flags a git binary chunk with no hunks", () => {
expect(isContentlessBinaryPatch([
"diff --git a/logo.png b/logo.png",
"index 1111111111aa..2222222222bb 100644",
"Binary files a/logo.png and b/logo.png differ",
"",
].join("\n"))).toBe(true);
});
test("flags a review stub for a file the server declined to read", () => {
expect(isContentlessBinaryPatch([
"diff --git a/src/Panel.tsx b/src/Panel.tsx",
"similarity index 94%",
"rename from src/Card.tsx",
"rename to src/Panel.tsx",
"index bab081fdb737..99fffbd3cac3 100644",
"Binary files a/src/Card.tsx and b/src/Panel.tsx differ",
"",
].join("\n"))).toBe(true);
});
test("flags a literal GIT binary patch payload", () => {
expect(isContentlessBinaryPatch([
"diff --git a/logo.png b/logo.png",
"GIT binary patch",
"literal 12",
"",
].join("\n"))).toBe(true);
});
test("does not flag a text patch", () => {
expect(isContentlessBinaryPatch([
"diff --git a/calc.ts b/calc.ts",
"--- a/calc.ts",
"+++ b/calc.ts",
"@@ -1,2 +1,2 @@",
"-const b = 1;",
"+const b = 2;",
"",
].join("\n"))).toBe(false);
});
test("does not flag a text patch whose content mentions the binary marker", () => {
// Content lines always carry a +/-/space prefix, and the scan stops at the
// first hunk header, so quoted marker text cannot be mistaken for a header.
expect(isContentlessBinaryPatch([
"diff --git a/notes.md b/notes.md",
"--- a/notes.md",
"+++ b/notes.md",
"@@ -1,2 +1,2 @@",
"-old note",
"+Binary files a/x and b/x differ",
" GIT binary patch",
"",
].join("\n"))).toBe(false);
});
test("does not flag a metadata-only chunk with no binary marker", () => {
// A pure mode change has no body either, but git says nothing about
// content there, so it keeps its existing rendering.
expect(isContentlessBinaryPatch([
"diff --git a/run.sh b/run.sh",
"old mode 100644",
"new mode 100755",
"",
].join("\n"))).toBe(false);
});
});
describe("diff path parsing", () => {
test("unquoteGitPath decodes octal (UTF-8 byte) escapes", () => {
// git C-quotes non-ASCII names as raw UTF-8 bytes in octal — the exact
// form ls-files/status/diff emit for "café.txt" with core.quotePath on.
expect(unquoteGitPath('"caf\\303\\251.txt"')).toBe("café.txt");
expect(unquoteGitPath('"\\346\\227\\245\\346\\234\\254.md"')).toBe("日本.md");
expect(unquoteGitPath('"tab\\there"')).toBe("tab\there");
expect(unquoteGitPath('"quote\\"in name"')).toBe('quote"in name');
// Our own quoteGitPath (JSON.stringify) leaves unicode LITERAL inside
// quotes — synthesized workspace headers round-trip through here too.
expect(unquoteGitPath('"café file.txt"')).toBe("café file.txt");
expect(unquoteGitPath('"emoji 🎉.txt"')).toBe("emoji 🎉.txt");
// …and emits \uXXXX for control chars without a short JSON escape.
expect(unquoteGitPath('"a\\u000bb.txt"')).toBe("a\u000bb.txt");
// Malformed \u (too few hex digits) keeps the backslash literally.
expect(unquoteGitPath('"a\\u0b.txt"')).toBe("a\\u0b.txt");
// Unquoted input passes through untouched.
expect(unquoteGitPath("plain space.txt")).toBe("plain space.txt");
});
test("strips tab metadata from unquoted file path lines", () => {
expect(parseDiffFilePathLines([
"--- a/my file\t",
"+++ b/my file\t",
"@@ -1 +1 @@",
])).toEqual({
oldPath: "my file",
newPath: "my file",
});
});
test("preserves escaped tabs inside quoted file paths", () => {
expect(parseDiffFilePathLines([
'--- "a/my\\tfile"',
'+++ "b/my\\tfile"',
"@@ -1 +1 @@",
])).toEqual({
oldPath: "my\tfile",
newPath: "my\tfile",
});
});
test("preserves dev null paths with tab metadata", () => {
expect(parsePatchPathToken("/dev/null\t", "a")).toBe("/dev/null");
});
});