238 lines
7.2 KiB
TypeScript
238 lines
7.2 KiB
TypeScript
/**
|
||
* Tests for the noise-filtered diff engine (src/diff.ts).
|
||
* Covers: per-file +/− parsing, excluded-file split, totals excluding noise,
|
||
* malformed-chunk guard, isExcluded reasons, and configurable overrides.
|
||
*/
|
||
|
||
import { describe, test, expect } from "bun:test";
|
||
import {
|
||
parseDiff,
|
||
filterNoise,
|
||
isExcluded,
|
||
compileIgnorePatterns,
|
||
EXCLUDED_PATTERNS,
|
||
} from "../src/diff";
|
||
|
||
/** A synthetic unified diff mixing code, a lockfile, a minified file, and a binary. */
|
||
const SYNTH_DIFF = [
|
||
"diff --git a/src/index.ts b/src/index.ts",
|
||
"index 111..222 100644",
|
||
"--- a/src/index.ts",
|
||
"+++ b/src/index.ts",
|
||
"@@ -1,2 +1,4 @@",
|
||
' import { foo } from "./foo";',
|
||
"+export const baz = 1;",
|
||
"+export const qux = 2;",
|
||
"-foo();",
|
||
"+bar();",
|
||
"",
|
||
"diff --git a/package-lock.json b/package-lock.json",
|
||
"index 000..111 100644",
|
||
"--- a/package-lock.json",
|
||
"+++ b/package-lock.json",
|
||
"@@ -0,0 +1,3 @@",
|
||
"+{",
|
||
'+ "name": "x"',
|
||
"+}",
|
||
"",
|
||
"diff --git a/dist/foo.min.js b/dist/foo.min.js",
|
||
"index 111..222 100644",
|
||
"--- a/dist/foo.min.js",
|
||
"+++ b/dist/foo.min.js",
|
||
"@@ -1 +1 @@",
|
||
"-var a=1;",
|
||
"+var a=2;",
|
||
"",
|
||
"diff --git a/assets/logo.png b/assets/logo.png",
|
||
"index 111..222 100644",
|
||
"Binary files differ",
|
||
].join("\n");
|
||
|
||
describe("parseDiff", () => {
|
||
test("splits included vs excluded files and totals only included", () => {
|
||
const summary = parseDiff(SYNTH_DIFF);
|
||
|
||
// Included: only src/index.ts (code). Lockfile, minified, binary excluded.
|
||
expect(summary.files).toHaveLength(1);
|
||
expect(summary.files[0]).toEqual({
|
||
path: "src/index.ts",
|
||
linesAdded: 3,
|
||
linesRemoved: 1,
|
||
ext: "ts",
|
||
});
|
||
|
||
expect(summary.excluded).toHaveLength(3);
|
||
const byPath = new Map(
|
||
summary.excluded.map((f) => [f.path, f]),
|
||
);
|
||
expect(byPath.get("package-lock.json")).toMatchObject({
|
||
linesAdded: 3,
|
||
linesRemoved: 0,
|
||
reason: "lockfile",
|
||
});
|
||
expect(byPath.get("dist/foo.min.js")).toMatchObject({
|
||
linesAdded: 1,
|
||
linesRemoved: 1,
|
||
reason: "minified asset",
|
||
});
|
||
expect(byPath.get("assets/logo.png")).toMatchObject({
|
||
linesAdded: 0,
|
||
linesRemoved: 0,
|
||
reason: "binary/media asset",
|
||
});
|
||
|
||
// Totals exclude the noise files.
|
||
expect(summary.totalAdded).toBe(3);
|
||
expect(summary.totalRemoved).toBe(1);
|
||
});
|
||
|
||
test("returns empty summary for an empty diff", () => {
|
||
const summary = parseDiff("");
|
||
expect(summary.files).toHaveLength(0);
|
||
expect(summary.excluded).toHaveLength(0);
|
||
expect(summary.totalAdded).toBe(0);
|
||
expect(summary.totalRemoved).toBe(0);
|
||
});
|
||
|
||
test("skips malformed chunks without a/… b/ header without crashing", () => {
|
||
const malformed =
|
||
"diff --git weird-line\nindex 111..222\n--- a/x\n+++ b/x\n+x\n" +
|
||
"\n" +
|
||
"diff --git a/src/ok.ts b/src/ok.ts\n--- a/src/ok.ts\n+++ b/src/ok.ts\n+ok\n";
|
||
const summary = parseDiff(malformed);
|
||
// Only the well-formed chunk is counted.
|
||
expect(summary.files).toHaveLength(1);
|
||
expect(summary.files[0].path).toBe("src/ok.ts");
|
||
expect(summary.totalAdded).toBe(1);
|
||
});
|
||
|
||
test("does not count +++/--- header lines as additions/removals", () => {
|
||
const diff = [
|
||
"diff --git a/src/a.ts b/src/a.ts",
|
||
"--- a/src/a.ts",
|
||
"+++ b/src/a.ts",
|
||
"@@ -0,0 +1,2 @@",
|
||
"+one",
|
||
"+two",
|
||
].join("\n");
|
||
const summary = parseDiff(diff);
|
||
expect(summary.files[0].linesAdded).toBe(2);
|
||
expect(summary.files[0].linesRemoved).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe("isExcluded", () => {
|
||
test("returns the right reason per pattern", () => {
|
||
expect(isExcluded("package-lock.json")).toBe("lockfile");
|
||
expect(isExcluded("yarn.lock")).toBe("lockfile");
|
||
expect(isExcluded("src/app.min.js")).toBe("minified asset");
|
||
expect(isExcluded("src/styles.min.css")).toBe("minified asset");
|
||
expect(isExcluded("build/out.js")).toBe("build output");
|
||
expect(isExcluded("node_modules/foo/index.js")).toBe("dependency");
|
||
expect(isExcluded("vendor/lib.bundle.js")).toBe("vendored dependency");
|
||
expect(isExcluded("assets/icon.svg")).toBe("binary/media asset");
|
||
expect(isExcluded("src/api.generated.ts")).toBe("generated file");
|
||
expect(isExcluded("test/__snapshots__/x.snap")).toBe("snapshot");
|
||
expect(isExcluded("dist/x.js.map")).toBe("source map");
|
||
});
|
||
|
||
test("returns undefined for review-relevant files", () => {
|
||
expect(isExcluded("src/foo.ts")).toBeUndefined();
|
||
expect(isExcluded("src/index.ts")).toBeUndefined();
|
||
});
|
||
|
||
test("merges caller-supplied extra patterns", () => {
|
||
expect(isExcluded("src/data.foo", [/\.foo$/])).toBe("extra ignore pattern");
|
||
expect(isExcluded("src/data.foo")).toBeUndefined();
|
||
});
|
||
|
||
test("EXCLUDED_PATTERNS covers lockfiles, min, generated, snap, map, build, vendor, binaries", () => {
|
||
for (const pat of [
|
||
"package-lock.json",
|
||
"src/app.min.js",
|
||
"src/thing.generated.ts",
|
||
"x.snap",
|
||
"x.js.map",
|
||
"dist/bundle.js",
|
||
"node_modules/a/b.js",
|
||
"vendor/x",
|
||
"a.png",
|
||
"f.woff2",
|
||
]) {
|
||
const hit = EXCLUDED_PATTERNS.some((r) => r.pattern.test(pat));
|
||
expect(hit, `${pat} should be covered by a default rule`).toBe(true);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("filterNoise", () => {
|
||
test("re-emits only included-file chunks", () => {
|
||
const filtered = filterNoise(SYNTH_DIFF);
|
||
expect(filtered).toContain("diff --git a/src/index.ts");
|
||
expect(filtered).not.toContain("package-lock.json");
|
||
expect(filtered).not.toContain("foo.min.js");
|
||
expect(filtered).not.toContain("logo.png");
|
||
});
|
||
|
||
test("returns empty when every file is noise", () => {
|
||
const onlyNoise = [
|
||
"diff --git a/package-lock.json b/package-lock.json",
|
||
"--- a/package-lock.json",
|
||
"+++ b/package-lock.json",
|
||
"+x",
|
||
].join("\n");
|
||
expect(filterNoise(onlyNoise)).toBe("");
|
||
});
|
||
});
|
||
|
||
describe("configurable noise rules", () => {
|
||
test("extraPatterns excludes a matching file from the review diff", () => {
|
||
const diff = [
|
||
"diff --git a/src/foo.ts b/src/foo.ts",
|
||
"--- a/src/foo.ts",
|
||
"+++ b/src/foo.ts",
|
||
"+keep",
|
||
"diff --git a/src/data.foo b/src/data.foo",
|
||
"--- a/src/data.foo",
|
||
"+++ b/src/data.foo",
|
||
"+drop",
|
||
].join("\n");
|
||
const opts = { extraPatterns: compileIgnorePatterns(["\\.foo$"]) };
|
||
const summary = parseDiff(diff, opts);
|
||
expect(summary.files.map((f) => f.path)).toEqual(["src/foo.ts"]);
|
||
expect(summary.excluded.map((f) => [f.path, f.reason])).toEqual([
|
||
["src/data.foo", "extra ignore pattern"],
|
||
]);
|
||
expect(filterNoise(diff, opts)).not.toContain("data.foo");
|
||
});
|
||
|
||
test("ignorePaths keeps an excluded-by-default file in scope", () => {
|
||
const diff = [
|
||
"diff --git a/package-lock.json b/package-lock.json",
|
||
"--- a/package-lock.json",
|
||
"+++ b/package-lock.json",
|
||
"+a",
|
||
"+b",
|
||
"+c",
|
||
].join("\n");
|
||
const opts = { ignorePaths: ["package-lock.json"] };
|
||
const summary = parseDiff(diff, opts);
|
||
expect(summary.files).toHaveLength(1);
|
||
expect(summary.files[0].path).toBe("package-lock.json");
|
||
expect(summary.excluded).toHaveLength(0);
|
||
expect(summary.totalAdded).toBe(3);
|
||
expect(filterNoise(diff, opts)).toContain("package-lock.json");
|
||
});
|
||
|
||
test("default behavior unchanged when overrides are unset", () => {
|
||
const summary = parseDiff(SYNTH_DIFF);
|
||
expect(summary.files[0].path).toBe("src/index.ts");
|
||
expect(summary.totalAdded).toBe(3);
|
||
});
|
||
|
||
test("compileIgnorePatterns skips invalid regexes", () => {
|
||
const compiled = compileIgnorePatterns(["\\.foo$", "(", "ok$"]);
|
||
expect(compiled.length).toBe(2);
|
||
});
|
||
});
|