import { describe, expect, it } from "vite-plus/test"; import { filterUnifiedDiffFiles, parseTurnDiffFilesFromUnifiedDiff } from "./Diffs.ts"; describe("parseTurnDiffFilesFromUnifiedDiff", () => { it("returns empty list for empty diff", () => { expect(parseTurnDiffFilesFromUnifiedDiff("")).toEqual([]); }); it("parses per-file additions and deletions", () => { const diff = [ "diff --git a/a.txt b/a.txt", "index 1111111..2222222 100644", "--- a/a.txt", "+++ b/a.txt", "@@ -1,2 +1,3 @@", " one", "-two", "+two updated", "+three", "diff --git a/src/b.ts b/src/b.ts", "index 3333333..4444444 100644", "--- a/src/b.ts", "+++ b/src/b.ts", "@@ -3,2 +3,0 @@", "-old", "-stale", "", ].join("\n"); expect(parseTurnDiffFilesFromUnifiedDiff(diff)).toEqual([ { path: "a.txt", additions: 2, deletions: 1 }, { path: "src/b.ts", additions: 0, deletions: 2 }, ]); }); it("parses rename-only diffs with zero line changes", () => { const diff = [ "diff --git a/src/old.ts b/src/new.ts", "similarity index 100%", "rename from src/old.ts", "rename to src/new.ts", "", ].join("\n"); expect(parseTurnDiffFilesFromUnifiedDiff(diff)).toEqual([ { path: "src/new.ts", additions: 0, deletions: 0 }, ]); }); it("normalizes CRLF input before parsing", () => { const diff = [ "diff --git a/a.txt b/a.txt", "index 1111111..2222222 100644", "--- a/a.txt", "+++ b/a.txt", "@@ -1 +1,2 @@", "-one", "+one updated", "+two", "", ].join("\r\n"); expect(parseTurnDiffFilesFromUnifiedDiff(diff)).toEqual([ { path: "a.txt", additions: 2, deletions: 1 }, ]); });}); describe("filterUnifiedDiffFiles", () => { const twoFileDiff = [ "diff --git a/kept.ts b/kept.ts", "index 1111111..2222222 100644", "--- a/kept.ts", "+++ b/kept.ts", "@@ -1,1 +1,1 @@", "-old", "+new", "diff --git a/dropped.ts b/dropped.ts", "index 3333333..4444444 100644", "--- a/dropped.ts", "+++ b/dropped.ts", "@@ -1,1 +1,1 @@", "-before", "+after", "", ].join("\n"); it("removes sections whose path is rejected", () => { const filtered = filterUnifiedDiffFiles(twoFileDiff, (path) => path !== "dropped.ts"); expect(filtered).toContain("kept.ts"); expect(filtered).not.toContain("dropped.ts"); expect(parseTurnDiffFilesFromUnifiedDiff(filtered)).toEqual([ { path: "kept.ts", additions: 1, deletions: 1 }, ]); }); it("returns the diff unchanged when everything is kept", () => { expect(filterUnifiedDiffFiles(twoFileDiff, () => true)).toBe(twoFileDiff); }); it("filters deleted files via the --- a/ path", () => { const deletionDiff = [ "diff --git a/removed.ts b/removed.ts", "deleted file mode 100644", "index 1111111..0000000", "--- a/removed.ts", "+++ /dev/null", "@@ -1,1 +0,0 @@", "-gone", "", ].join("\n"); expect(filterUnifiedDiffFiles(deletionDiff, (path) => path !== "removed.ts").trim()).toBe(""); }); it("filters simple header-only sections via the diff --git header", () => { const oddDiff = ["diff --git a/x b/x", "Binary files differ", ""].join("\n"); expect(filterUnifiedDiffFiles(oddDiff, (path) => path !== "x").trim()).toBe(""); }); it("keeps unparseable sections", () => { const oddDiff = ["diff --git malformed-header", "Binary files differ", ""].join("\n"); expect(filterUnifiedDiffFiles(oddDiff, () => false)).toBe(oddDiff); }); it("returns empty diff unchanged", () => { expect(filterUnifiedDiffFiles("", () => false)).toBe(""); });}); describe("resolveDiffSectionPath via renames", () => { it("filters rename sections by the post-image path", () => { const renameDiff = [ "diff --git a/old/name.ts b/new/name.ts", "similarity index 90%", "rename from old/name.ts", "rename to new/name.ts", "index 1111111..2222222 100644", "--- a/old/name.ts", "+++ b/new/name.ts", "@@ -1,1 +1,1 @@", "-before", "+after", "", ].join("\n"); // The attribution map is keyed by post-image paths; a filter that only // knows the new path must still match this section. expect(filterUnifiedDiffFiles(renameDiff, (path) => path !== "new/name.ts").trim()).toBe(""); expect(filterUnifiedDiffFiles(renameDiff, (path) => path !== "old/name.ts")).toBe(renameDiff); });}); describe("filterUnifiedDiffFiles fallback paths", () => { it("filters binary sections via the diff --git header", () => { const binaryDiff = [ "diff --git a/assets/logo.png b/assets/logo.png", "index 1111111..2222222 100644", "Binary files a/assets/logo.png and b/assets/logo.png differ", "", ].join("\n"); expect(filterUnifiedDiffFiles(binaryDiff, (path) => path !== "assets/logo.png").trim()).toBe( "", ); expect(filterUnifiedDiffFiles(binaryDiff, () => true)).toBe(binaryDiff); }); it("filters mode-only sections via the diff --git header", () => { const modeDiff = [ "diff --git a/scripts/run.sh b/scripts/run.sh", "old mode 100644", "new mode 100755", "", ].join("\n"); expect(filterUnifiedDiffFiles(modeDiff, (path) => path !== "scripts/run.sh").trim()).toBe(""); }); it("filters binary sections with spaces in the path", () => { const binaryDiff = [ "diff --git a/my assets/logo file.png b/my assets/logo file.png", "Binary files differ", "", ].join("\n"); expect( filterUnifiedDiffFiles(binaryDiff, (path) => path !== "my assets/logo file.png").trim(), ).toBe(""); }); it("decodes git C-style quoted paths", () => { const quotedDiff = [ 'diff --git "a/sp\\303\\244ce.ts" "b/sp\\303\\244ce.ts"', "index 1111111..2222222 100644", '--- "a/sp\\303\\244ce.ts"', '+++ "b/sp\\303\\244ce.ts"', "@@ -1,1 +1,1 @@", "-old", "+new", "", ].join("\n"); // git octal-escapes the raw UTF-8 bytes (`ä` = \303\244); the decoder // must reassemble them into the real character, not per-byte mojibake. expect(filterUnifiedDiffFiles(quotedDiff, (path) => path !== "späce.ts").trim()).toBe(""); }); it("filters rename-only sections without hunks via rename metadata", () => { const renameOnlyDiff = [ "diff --git a/old.ts b/new.ts", "similarity index 100%", "rename from old.ts", "rename to new.ts", "", ].join("\n"); expect(filterUnifiedDiffFiles(renameOnlyDiff, (path) => path !== "new.ts").trim()).toBe(""); expect(filterUnifiedDiffFiles(renameOnlyDiff, (path) => path !== "old.ts")).toBe( renameOnlyDiff, ); }); it("keeps ambiguous unquoted rename headers", () => { // Rename of a space-containing path is ambiguous in the header when git // does not quote; with no other metadata the section must be kept. const ambiguous = ["diff --git a/x y b/y z", "Binary files differ", ""].join("\n"); expect(filterUnifiedDiffFiles(ambiguous, () => false)).toBe(ambiguous); });});
import { describe, expect, it } from "vite-plus/test";
import { filterUnifiedDiffFiles, parseTurnDiffFilesFromUnifiedDiff } from "./Diffs.ts";
describe("parseTurnDiffFilesFromUnifiedDiff", () => {
it("returns empty list for empty diff", () => {
expect(parseTurnDiffFilesFromUnifiedDiff("")).toEqual([]);
});
it("parses per-file additions and deletions", () => {
const diff = [
"diff --git a/a.txt b/a.txt",
"index 1111111..2222222 100644",
"--- a/a.txt",
"+++ b/a.txt",
"@@ -1,2 +1,3 @@",
" one",
"-two",
"+two updated",
"+three",
"diff --git a/src/b.ts b/src/b.ts",
"index 3333333..4444444 100644",
"--- a/src/b.ts",
"+++ b/src/b.ts",
"@@ -3,2 +3,0 @@",
"-old",
"-stale",
"",
].join("\n");
expect(parseTurnDiffFilesFromUnifiedDiff(diff)).toEqual([
{ path: "a.txt", additions: 2, deletions: 1 },
{ path: "src/b.ts", additions: 0, deletions: 2 },
]);
});
it("parses rename-only diffs with zero line changes", () => {
const diff = [
"diff --git a/src/old.ts b/src/new.ts",
"similarity index 100%",
"rename from src/old.ts",
"rename to src/new.ts",
"",
].join("\n");
expect(parseTurnDiffFilesFromUnifiedDiff(diff)).toEqual([
{ path: "src/new.ts", additions: 0, deletions: 0 },
]);
});
it("normalizes CRLF input before parsing", () => {
const diff = [
"diff --git a/a.txt b/a.txt",
"index 1111111..2222222 100644",
"--- a/a.txt",
"+++ b/a.txt",
"@@ -1 +1,2 @@",
"-one",
"+one updated",
"+two",
"",
].join("\r\n");
expect(parseTurnDiffFilesFromUnifiedDiff(diff)).toEqual([
{ path: "a.txt", additions: 2, deletions: 1 },
]);
});
});
describe("filterUnifiedDiffFiles", () => {
const twoFileDiff = [
"diff --git a/kept.ts b/kept.ts",
"index 1111111..2222222 100644",
"--- a/kept.ts",
"+++ b/kept.ts",
"@@ -1,1 +1,1 @@",
"-old",
"+new",
"diff --git a/dropped.ts b/dropped.ts",
"index 3333333..4444444 100644",
"--- a/dropped.ts",
"+++ b/dropped.ts",
"@@ -1,1 +1,1 @@",
"-before",
"+after",
"",
].join("\n");
it("removes sections whose path is rejected", () => {
const filtered = filterUnifiedDiffFiles(twoFileDiff, (path) => path !== "dropped.ts");
expect(filtered).toContain("kept.ts");
expect(filtered).not.toContain("dropped.ts");
expect(parseTurnDiffFilesFromUnifiedDiff(filtered)).toEqual([
{ path: "kept.ts", additions: 1, deletions: 1 },
]);
});
it("returns the diff unchanged when everything is kept", () => {
expect(filterUnifiedDiffFiles(twoFileDiff, () => true)).toBe(twoFileDiff);
});
it("filters deleted files via the --- a/ path", () => {
const deletionDiff = [
"diff --git a/removed.ts b/removed.ts",
"deleted file mode 100644",
"index 1111111..0000000",
"--- a/removed.ts",
"+++ /dev/null",
"@@ -1,1 +0,0 @@",
"-gone",
"",
].join("\n");
expect(filterUnifiedDiffFiles(deletionDiff, (path) => path !== "removed.ts").trim()).toBe("");
});
it("filters simple header-only sections via the diff --git header", () => {
const oddDiff = ["diff --git a/x b/x", "Binary files differ", ""].join("\n");
expect(filterUnifiedDiffFiles(oddDiff, (path) => path !== "x").trim()).toBe("");
});
it("keeps unparseable sections", () => {
const oddDiff = ["diff --git malformed-header", "Binary files differ", ""].join("\n");
expect(filterUnifiedDiffFiles(oddDiff, () => false)).toBe(oddDiff);
});
it("returns empty diff unchanged", () => {
expect(filterUnifiedDiffFiles("", () => false)).toBe("");
});
});
describe("resolveDiffSectionPath via renames", () => {
it("filters rename sections by the post-image path", () => {
const renameDiff = [
"diff --git a/old/name.ts b/new/name.ts",
"similarity index 90%",
"rename from old/name.ts",
"rename to new/name.ts",
"index 1111111..2222222 100644",
"--- a/old/name.ts",
"+++ b/new/name.ts",
"@@ -1,1 +1,1 @@",
"-before",
"+after",
"",
].join("\n");
// The attribution map is keyed by post-image paths; a filter that only
// knows the new path must still match this section.
expect(filterUnifiedDiffFiles(renameDiff, (path) => path !== "new/name.ts").trim()).toBe("");
expect(filterUnifiedDiffFiles(renameDiff, (path) => path !== "old/name.ts")).toBe(renameDiff);
});
});
describe("filterUnifiedDiffFiles fallback paths", () => {
it("filters binary sections via the diff --git header", () => {
const binaryDiff = [
"diff --git a/assets/logo.png b/assets/logo.png",
"index 1111111..2222222 100644",
"Binary files a/assets/logo.png and b/assets/logo.png differ",
"",
].join("\n");
expect(filterUnifiedDiffFiles(binaryDiff, (path) => path !== "assets/logo.png").trim()).toBe(
"",
);
expect(filterUnifiedDiffFiles(binaryDiff, () => true)).toBe(binaryDiff);
});
it("filters mode-only sections via the diff --git header", () => {
const modeDiff = [
"diff --git a/scripts/run.sh b/scripts/run.sh",
"old mode 100644",
"new mode 100755",
"",
].join("\n");
expect(filterUnifiedDiffFiles(modeDiff, (path) => path !== "scripts/run.sh").trim()).toBe("");
});
it("filters binary sections with spaces in the path", () => {
const binaryDiff = [
"diff --git a/my assets/logo file.png b/my assets/logo file.png",
"Binary files differ",
"",
].join("\n");
expect(
filterUnifiedDiffFiles(binaryDiff, (path) => path !== "my assets/logo file.png").trim(),
).toBe("");
});
it("decodes git C-style quoted paths", () => {
const quotedDiff = [
'diff --git "a/sp\\303\\244ce.ts" "b/sp\\303\\244ce.ts"',
"index 1111111..2222222 100644",
'--- "a/sp\\303\\244ce.ts"',
'+++ "b/sp\\303\\244ce.ts"',
"@@ -1,1 +1,1 @@",
"-old",
"+new",
"",
].join("\n");
// git octal-escapes the raw UTF-8 bytes (`ä` = \303\244); the decoder
// must reassemble them into the real character, not per-byte mojibake.
expect(filterUnifiedDiffFiles(quotedDiff, (path) => path !== "späce.ts").trim()).toBe("");
});
it("filters rename-only sections without hunks via rename metadata", () => {
const renameOnlyDiff = [
"diff --git a/old.ts b/new.ts",
"similarity index 100%",
"rename from old.ts",
"rename to new.ts",
"",
].join("\n");
expect(filterUnifiedDiffFiles(renameOnlyDiff, (path) => path !== "new.ts").trim()).toBe("");
expect(filterUnifiedDiffFiles(renameOnlyDiff, (path) => path !== "old.ts")).toBe(
renameOnlyDiff,
);
});
it("keeps ambiguous unquoted rename headers", () => {
// Rename of a space-containing path is ambiguous in the header when git
// does not quote; with no other metadata the section must be kept.
const ambiguous = ["diff --git a/x y b/y z", "Binary files differ", ""].join("\n");
expect(filterUnifiedDiffFiles(ambiguous, () => false)).toBe(ambiguous);
});
});