Files
git-tools/tests/git/tools.test.ts
T
2026-09-01 13:05:57 +02:00

127 lines
4.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildConventionalCommit,
compareSemVer,
createUnifiedPatch,
explainGitignore,
lintConventionalCommit,
normalizeChangelog,
parseSemVer,
parseUnifiedPatch,
satisfiesSemVer,
} from "../../src/git/tools";
describe("Git interchange tools", () => {
it("parses patch files, renames, hunks, and statistics", () => {
const source = `diff --git a/old.txt b/new.txt\nsimilarity index 90%\nrename from old.txt\nrename to new.txt\n--- a/old.txt\n+++ b/new.txt\n@@ -1,2 +1,2 @@ section\n same\n-old\n+new\n`;
const parsed = parseUnifiedPatch(source);
expect(parsed).toMatchObject({ added: 1, deleted: 1 });
expect(parsed.files[0]).toMatchObject({
status: "renamed",
oldPath: "old.txt",
newPath: "new.txt",
similarity: 90,
});
expect(parsed.files[0]?.hunks[0]?.validCounts).toBe(true);
});
it("decodes Git C-quoted octal paths", () => {
const source = `diff --git "a/foo\\040bar.txt" "b/foo\\040bar.txt"\n--- "a/foo\\040bar.txt"\n+++ "b/foo\\040bar.txt"\n@@ -1 +1 @@\n-old\n+new\n`;
expect(parseUnifiedPatch(source).files[0]).toMatchObject({
oldPath: "foo bar.txt",
newPath: "foo bar.txt",
});
});
it("authors a patch that parses with valid counts", () => {
const output = createUnifiedPatch(
"a\nb\nc\n",
"a\nB\nc\nd\n",
"src/a file.txt",
1,
);
expect(output).toContain("-b");
expect(output).toContain("+B");
const parsed = parseUnifiedPatch(output);
expect(parsed.diagnostics).toEqual([]);
expect(parsed).toMatchObject({ added: 2, deleted: 1 });
});
it("explains ordered ignore rules and blocked parents", () => {
const source = `dist/\n*.log\n!keep.log\ncache/\n!cache/readme.md\n`;
const results = explainGitignore(source, [
"dist/app.js",
"debug.log",
"keep.log",
"cache/readme.md",
]);
expect(results.map((item) => item.ignored)).toEqual([
true,
true,
false,
true,
]);
expect(results[3]?.explanation).toMatch(/parent cache/u);
});
it("propagates ordinary directory matches but respects directory-only inputs", () => {
const results = explainGitignore(`dist/\nbuild\n`, [
"dist",
"dist/",
"dist/app.js",
"build",
"build/",
"build/app.js",
]);
expect(results.map((item) => item.ignored)).toEqual([
false,
true,
true,
true,
true,
true,
]);
});
it("implements SemVer precedence and basic ranges", () => {
expect(compareSemVer("1.0.0-alpha.1", "1.0.0-alpha.beta")).toBeLessThan(0);
expect(compareSemVer("1.0.0+one", "1.0.0+two")).toBe(0);
expect(satisfiesSemVer("1.4.2", "^1.2.0 <2.0.0")).toBe(true);
expect(satisfiesSemVer("2.0.0", "^1.2.0 || >=3.0.0")).toBe(false);
expect(satisfiesSemVer("1.3.0-beta.1", "^1.2.0")).toBe(false);
expect(satisfiesSemVer("1.9.9", "~1")).toBe(true);
expect(satisfiesSemVer("0.8.0", "^0")).toBe(true);
expect(satisfiesSemVer("0.0.9", "^0.0")).toBe(true);
expect(satisfiesSemVer("2.3.9", "1.2 - 2.3")).toBe(true);
expect(satisfiesSemVer("1.99.0", "<=1")).toBe(true);
expect(satisfiesSemVer("2.0.0", "<=1")).toBe(false);
expect(() => parseSemVer("9007199254740992.0.0")).toThrow(/safe integer/u);
});
it("builds and lints conventional commits", () => {
const message = buildConventionalCommit({
type: "feat",
scope: "api",
breaking: true,
subject: "change response shape",
breakingDescription: "Clients must read data.items.",
issues: "#42",
});
expect(message).toContain("feat(api)!");
expect(message).toContain("BREAKING CHANGE:");
expect(lintConventionalCommit(message).valid).toBe(true);
expect(lintConventionalCommit("bad header").valid).toBe(false);
});
it("normalizes changelog headings, order, and duplicate bullets", () => {
const result = normalizeChangelog(
`# Changelog\n\n## 1.0.0 - 2026-01-01\n\n### fixed\n\n- B\n- B\n\n### Added\n\n- A\n`,
);
expect(result.deduplicated).toBe(1);
expect(result.output.indexOf("### Added")).toBeLessThan(
result.output.indexOf("### Fixed"),
);
expect(result.output).toContain("## [1.0.0] - 2026-01-01");
});
});