83c3c1ccf1
Co-authored-by: Kandev Agent <agent@kandev.dev>
137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { lineInRanges, parseAddedLineRanges } from "./diff-ranges.mjs";
|
|
|
|
const OLD_A = "--- a/a.tsx";
|
|
const NEW_A = "+++ b/a.tsx";
|
|
|
|
describe("parseAddedLineRanges", () => {
|
|
it("reads a single-line hunk with no explicit count", () => {
|
|
const diff = [OLD_A, NEW_A, "@@ -4 +4 @@", "-old", "+new"].join("\n");
|
|
expect(parseAddedLineRanges(diff).get("a.tsx")).toEqual([[4, 4]]);
|
|
});
|
|
|
|
it("reads a multi-line hunk", () => {
|
|
const diff = [OLD_A, NEW_A, "@@ -10,0 +11,3 @@", "+x", "+y", "+z"].join("\n");
|
|
expect(parseAddedLineRanges(diff).get("a.tsx")).toEqual([[11, 13]]);
|
|
});
|
|
|
|
it("ignores deletion-only hunks, which add no attributable line", () => {
|
|
const diff = [OLD_A, NEW_A, "@@ -5,2 +4,0 @@", "-gone", "-also"].join("\n");
|
|
expect(parseAddedLineRanges(diff).get("a.tsx")).toEqual([]);
|
|
});
|
|
|
|
it("ignores deleted files", () => {
|
|
const diff = ["--- a/gone.tsx", "+++ /dev/null", "@@ -1,2 +0,0 @@", "-a", "-b"].join("\n");
|
|
expect(parseAddedLineRanges(diff).has("gone.tsx")).toBe(false);
|
|
});
|
|
|
|
it("keeps hunks separate across several files", () => {
|
|
const diff = [
|
|
"diff --git a/a.tsx b/a.tsx",
|
|
OLD_A,
|
|
NEW_A,
|
|
"@@ -1 +1 @@",
|
|
"+a",
|
|
"diff --git a/b.tsx b/b.tsx",
|
|
"--- a/b.tsx",
|
|
"+++ b/b.tsx",
|
|
"@@ -20,0 +21,2 @@",
|
|
"+b",
|
|
"+c",
|
|
].join("\n");
|
|
const ranges = parseAddedLineRanges(diff);
|
|
expect(ranges.get("a.tsx")).toEqual([[1, 1]]);
|
|
expect(ranges.get("b.tsx")).toEqual([[21, 22]]);
|
|
});
|
|
|
|
it("collects multiple hunks in one file", () => {
|
|
const diff = [OLD_A, NEW_A, "@@ -1 +1 @@", "+a", "@@ -50,0 +51,2 @@", "+b", "+c"].join("\n");
|
|
expect(parseAddedLineRanges(diff).get("a.tsx")).toEqual([
|
|
[1, 1],
|
|
[51, 52],
|
|
]);
|
|
});
|
|
|
|
it("does not mistake a '+++' inside added content for a file header", () => {
|
|
// A line of added source that begins with "+++" arrives as "++++...".
|
|
const diff = [OLD_A, NEW_A, "@@ -1,0 +1,1 @@", "++++ not a header"].join("\n");
|
|
const ranges = parseAddedLineRanges(diff);
|
|
expect([...ranges.keys()]).toEqual(["a.tsx"]);
|
|
expect(ranges.get("a.tsx")).toEqual([[1, 1]]);
|
|
});
|
|
|
|
it("returns nothing for an empty diff", () => {
|
|
expect(parseAddedLineRanges("").size).toBe(0);
|
|
});
|
|
|
|
it("ignores a pure rename, which emits no headers or hunks", () => {
|
|
// A plain `git mv` must cost nothing: attributing the moved file's existing
|
|
// lines would demand a full migration for a file nobody edited.
|
|
const diff = [
|
|
"diff --git a/old.tsx b/new.tsx",
|
|
"similarity index 100%",
|
|
"rename from old.tsx",
|
|
"rename to new.tsx",
|
|
].join("\n");
|
|
expect(parseAddedLineRanges(diff).size).toBe(0);
|
|
});
|
|
|
|
it("does not attribute a later hunk to the file before a headerless entry", () => {
|
|
// A binary entry emits no `+++`, so without resetting on `diff --git` the
|
|
// previous file would stay selected and absorb the next hunk.
|
|
const diff = [
|
|
"diff --git a/a.tsx b/a.tsx",
|
|
OLD_A,
|
|
NEW_A,
|
|
"@@ -1 +1 @@",
|
|
"+a",
|
|
"diff --git a/img.png b/img.png",
|
|
"Binary files a/img.png and b/img.png differ",
|
|
"@@ -99,0 +99,3 @@",
|
|
"+not really a hunk",
|
|
].join("\n");
|
|
const ranges = parseAddedLineRanges(diff);
|
|
expect(ranges.get("a.tsx")).toEqual([[1, 1]]);
|
|
expect(ranges.has("img.png")).toBe(false);
|
|
});
|
|
|
|
it("keeps a rename-plus-edit scoped to the edited lines only", () => {
|
|
const diff = [
|
|
"diff --git a/old.tsx b/new.tsx",
|
|
"similarity index 92%",
|
|
"rename from old.tsx",
|
|
"rename to new.tsx",
|
|
"--- a/old.tsx",
|
|
"+++ b/new.tsx",
|
|
"@@ -2,0 +3 @@",
|
|
"+const added = 1;",
|
|
].join("\n");
|
|
expect(parseAddedLineRanges(diff).get("new.tsx")).toEqual([[3, 3]]);
|
|
});
|
|
});
|
|
|
|
describe("lineInRanges", () => {
|
|
const ranges: Array<[number, number]> = [
|
|
[3, 5],
|
|
[10, 10],
|
|
];
|
|
|
|
it("matches inclusive bounds", () => {
|
|
expect(lineInRanges(ranges, 3)).toBe(true);
|
|
expect(lineInRanges(ranges, 5)).toBe(true);
|
|
expect(lineInRanges(ranges, 10)).toBe(true);
|
|
});
|
|
|
|
it("rejects lines outside every range", () => {
|
|
expect(lineInRanges(ranges, 2)).toBe(false);
|
|
expect(lineInRanges(ranges, 6)).toBe(false);
|
|
expect(lineInRanges(ranges, 11)).toBe(false);
|
|
});
|
|
|
|
it("treats a missing range list as no match", () => {
|
|
// Reached when a modified file produced no attributable added lines.
|
|
expect(lineInRanges(undefined as unknown as Array<[number, number]>, 1)).toBe(false);
|
|
});
|
|
});
|