import { execFile } from "node:child_process"; import { chmod, copyFile, mkdir, mkdtemp, rm, unlink, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { delimiter, join } from "node:path"; import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; import { cleanGitEnvironment, collectSnapshot, isProtectedPath, MAX_PATCH_BYTES, parseArgs, } from "../git"; const execFileAsync = promisify(execFile); const roots: string[] = []; async function git(cwd: string, ...args: string[]): Promise { const { stdout } = await execFileAsync("git", args, { cwd, env: cleanGitEnvironment(), encoding: "utf8", maxBuffer: 2 * 1024 * 1024, }); return stdout.trim(); } async function repository(): Promise { const root = await mkdtemp(join(tmpdir(), "strata-git-")); roots.push(root); await git(root, "init", "--quiet"); await git(root, "config", "user.name", "Strata Test"); await git(root, "config", "user.email", "strata@example.invalid"); return root; } async function withFakeGit( root: string, program: string, check: () => Promise, ): Promise { const bin = join(root, "fake-bin"); await mkdir(bin); const executable = join( bin, process.platform === "win32" ? "git.exe" : "git", ); const previousOptions = process.env.NODE_OPTIONS; await copyFile(process.execPath, executable); await chmod(executable, 0o755); const preload = join(bin, "fake-git.cjs"); await writeFile( preload, `const original = process.argv.slice(1); original[0] = require("node:path").basename(original[0]); process.argv = [process.execPath, "fake-git", ...(process.execArgv.includes("-c") ? ["-c"] : []), ...original];\n${program}\nprocess.exit(process.exitCode ?? 0);`, ); process.env.NODE_OPTIONS = `--require=${JSON.stringify(preload)}`; const previous = process.env.PATH; process.env.PATH = `${bin}${delimiter}${previous ?? ""}`; try { await check(); } finally { if (previous === undefined) delete process.env.PATH; else process.env.PATH = previous; if (previousOptions === undefined) delete process.env.NODE_OPTIONS; else process.env.NODE_OPTIONS = previousOptions; } } async function commitAll(root: string, message: string): Promise { await git(root, "add", "--all"); await git(root, "commit", "--quiet", "-m", message); return git(root, "rev-parse", "HEAD"); } afterEach(async () => { await Promise.all( roots.splice(0).map((root) => rm(root, { recursive: true, force: true })), ); }); describe("parseArgs", () => { it("accepts only the documented sources and cancellation", () => { expect(parseArgs(" ")).toEqual({ kind: "working" }); expect(parseArgs("--staged")).toEqual({ kind: "staged" }); expect(parseArgs("--base trunk")).toEqual({ kind: "base", ref: "trunk" }); expect(parseArgs("--pr")).toEqual({ kind: "pr" }); expect(parseArgs("--pr 42")).toEqual({ kind: "pr", identifier: "42" }); expect(parseArgs("cancel")).toBe("cancel"); expect(() => parseArgs("--base")).toThrow(/usage/); expect(() => parseArgs("--base --output")).toThrow(/not an option/); expect(() => parseArgs("--staged extra")).toThrow(/usage/); }); }); describe("protected path filtering", () => { it("classifies pure path strings without creating protected fixtures", () => { expect(isProtectedPath("config/.env.production")).toBe(true); expect(isProtectedPath("config/.env.production/cache/value.txt")).toBe( true, ); expect(isProtectedPath("home/.ssh/id_ed25519")).toBe(true); expect(isProtectedPath("infra/secrets.auto.tfvars")).toBe(true); expect(isProtectedPath(".config/gh/hosts.yml")).toBe(true); expect(isProtectedPath(".pi/google-workspace/tokens/account.json")).toBe( true, ); expect(isProtectedPath("gcloud/legacy_credentials/account/token")).toBe( true, ); expect(isProtectedPath("pi/agent/auth.json")).toBe(true); expect(isProtectedPath("ssh/deploy.key")).toBe(true); expect(isProtectedPath("gh/hosts.yml")).toBe(true); expect(isProtectedPath("keys/service.pem")).toBe(true); expect(isProtectedPath("config/.env.example")).toBe(false); expect(isProtectedPath("config/.env.example/schema/value.txt")).toBe(false); expect(isProtectedPath("src/environment.ts")).toBe(false); }); }); describe("collectSnapshot", () => { it("captures text, additions, deletions, renames, metadata, and special paths while omitting binary and untracked files", async () => { const root = await repository(); await Promise.all([ writeFile(join(root, "modify.txt"), "before\nshared\n"), writeFile(join(root, "delete.txt"), "gone\n"), writeFile(join(root, "rename-old.txt"), "renamed\n"), writeFile(join(root, "script.sh"), "echo strata\n"), writeFile(join(root, "space ü.txt"), "old special\n"), writeFile(join(root, "-leading.txt"), "old leading\n"), writeFile(join(root, "binary.bin"), Buffer.from([0, 1, 2, 3])), ]); await commitAll(root, "initial"); await writeFile(join(root, "modify.txt"), "after\nshared\n"); await writeFile(join(root, "added.txt"), "added\n"); await git(root, "add", "--", "added.txt"); await unlink(join(root, "delete.txt")); await git(root, "mv", "--", "rename-old.txt", "rename new.txt"); await writeFile(join(root, "space ü.txt"), "new special\n"); await writeFile(join(root, "-leading.txt"), "new leading\n"); await writeFile(join(root, "binary.bin"), Buffer.from([0, 9, 8, 7])); await writeFile( join(root, "scratch.todo"), "untracked content is never read\n", ); const snapshot = await collectSnapshot(root, { kind: "working" }); const paths = new Set(snapshot.hunks.map((hunk) => hunk.path)); expect(paths).toEqual( new Set([ "-leading.txt", "added.txt", "binary.bin", "delete.txt", "modify.txt", "rename new.txt", "space ü.txt", ]), ); expect( snapshot.hunks.find((hunk) => hunk.path === "rename new.txt"), ).toMatchObject({ oldPath: "rename-old.txt", header: "metadata", }); const modified = snapshot.hunks.find((hunk) => hunk.path === "modify.txt"); expect(modified?.lines).toEqual([ { kind: "delete", text: "before", oldLine: 1 }, { kind: "add", text: "after", newLine: 1 }, { kind: "context", text: "shared", oldLine: 2, newLine: 2 }, ]); expect( snapshot.hunks.find((hunk) => hunk.path === "binary.bin"), ).toMatchObject({ header: "metadata", lines: [ expect.objectContaining({ kind: "meta", text: expect.stringMatching(/^Binary files /), }), ], }); expect(snapshot.skipped).toEqual([ { path: "binary.bin", reason: "binary" }, { path: "scratch.todo", reason: "untracked" }, ]); expect(snapshot.base).toMatch(/^[0-9a-f]{40,64}$/); expect(snapshot.head).toBe(snapshot.base); expect(snapshot.id).toMatch(/^[0-9a-f]{64}$/); expect(new Set(snapshot.hunks.map((hunk) => hunk.id)).size).toBe( snapshot.hunks.length, ); const repeated = await collectSnapshot(root, { kind: "working" }); expect(repeated).toEqual(snapshot); await writeFile(join(root, "another.todo"), "also omitted\n"); const changed = await collectSnapshot(root, { kind: "working" }); expect(changed.id).not.toBe(snapshot.id); expect(changed.hunks.map((hunk) => hunk.id)).toEqual( snapshot.hunks.map((hunk) => hunk.id), ); }); it("pins the staged index and excludes unstaged changes", async () => { const root = await repository(); await writeFile(join(root, "staged.txt"), "old staged\n"); await writeFile(join(root, "unstaged.txt"), "old unstaged\n"); await writeFile(join(root, "script.sh"), "echo strata\n"); const commit = await commitAll(root, "initial"); await writeFile(join(root, "staged.txt"), "new staged\n"); await git(root, "add", "--", "staged.txt"); await git(root, "update-index", "--chmod=+x", "script.sh"); await writeFile(join(root, "unstaged.txt"), "new unstaged\n"); const snapshot = await collectSnapshot(root, { kind: "staged" }); expect(snapshot.base).toBe(commit); expect(snapshot.head).not.toBe(commit); expect(snapshot.hunks.map((hunk) => hunk.path)).toEqual([ "script.sh", "staged.txt", ]); expect(snapshot.hunks[0]).toMatchObject({ path: "script.sh", header: "metadata", lines: [ { kind: "meta", text: "old mode 100644" }, { kind: "meta", text: "new mode 100755" }, ], }); expect(snapshot.skipped).toEqual([]); }); it("preserves mode metadata alongside a textual hunk", async () => { const root = await repository(); await writeFile(join(root, "script.sh"), "old\n"); await commitAll(root, "initial"); await writeFile(join(root, "script.sh"), "new\n"); await git(root, "add", "--", "script.sh"); await git(root, "update-index", "--chmod=+x", "script.sh"); const snapshot = await collectSnapshot(root, { kind: "staged" }); expect(snapshot.hunks).toHaveLength(1); expect(snapshot.hunks[0]?.lines).toEqual([ { kind: "meta", text: "old mode 100644" }, { kind: "meta", text: "new mode 100755" }, { kind: "delete", text: "old", oldLine: 1 }, { kind: "add", text: "new", newLine: 1 }, ]); }); it("pins a merge-base-to-HEAD range and ignores working tree changes", async () => { const root = await repository(); await writeFile(join(root, "committed.txt"), "first\n"); const first = await commitAll(root, "first"); await writeFile(join(root, "committed.txt"), "second\n"); const head = await commitAll(root, "second"); await writeFile(join(root, "committed.txt"), "working only\n"); const snapshot = await collectSnapshot(root, { kind: "base", ref: first }); expect(snapshot.base).toBe(first); expect(snapshot.head).toBe(head); expect(snapshot.hunks).toHaveLength(1); expect(snapshot.hunks[0]?.lines).toContainEqual({ kind: "add", text: "second", newLine: 1, }); expect(snapshot.hunks[0]?.lines).not.toContainEqual( expect.objectContaining({ text: "working only" }), ); }); it("accepts a quoted Git header without requiring an invalid Win32 filename on disk", async () => { const root = await repository(); const path = 'safe "quoted"\tü.txt'; const patch = `diff --git ${JSON.stringify(`a/${path}`)} ${JSON.stringify(`b/${path}`)}\nindex 1..2 100644\n--- ${JSON.stringify(`a/${path}`)}\n+++ ${JSON.stringify(`b/${path}`)}\n@@ -1 +1 @@\n-old\n+new\n`; await withFakeGit( root, `const args = process.argv.slice(2); const out = value => process.stdout.write(value); if (args.includes("rev-parse")) out(args.includes("--show-toplevel") ? process.cwd() + "\\n" : "${"a".repeat(40)}\\n"); else if (args[0] === "ls-files") out(args.includes("--others") ? "" : ${JSON.stringify(`${path}\0`)}); else if (args[0] === "ls-tree") out(${JSON.stringify(`${path}\0`)}); else if (args.includes("--name-status")) out(${JSON.stringify(`M\0${path}\0`)}); else if (args.includes("--patch")) out(${JSON.stringify(patch)});`, async () => { const snapshot = await collectSnapshot(root, { kind: "working" }); expect(snapshot.hunks[0]).toMatchObject({ path, lines: [ { kind: "delete", text: "old", oldLine: 1 }, { kind: "add", text: "new", newLine: 1 }, ], }); }, ); }); it("rejects a fake Git patch whose quoted header does not identify its metadata path", async () => { const root = await repository(); await withFakeGit( root, `const args = process.argv.slice(2); const out = (value) => process.stdout.write(value); if (args.includes("rev-parse")) out(args.includes("--show-toplevel") ? process.cwd() + "\\n" : "${"a".repeat(40)}\\n"); else if (args[0] === "ls-files") out(args.includes("--others") ? "" : "safe \\"quoted\\"\\tü.txt\\0"); else if (args[0] === "ls-tree") out("safe \\"quoted\\"\\tü.txt\\0"); else if (args.includes("--name-status")) out("M\\0safe \\"quoted\\"\\tü.txt\\0"); else if (args.includes("--patch")) out("diff --git a/other.txt b/other.txt\\nindex 1..2 100644\\n--- a/other.txt\\n+++ b/other.txt\\n@@ -1 +1 @@\\n-old\\n+new\\n");`, async () => { await expect( collectSnapshot(root, { kind: "working" }), ).rejects.toThrow(/patch header does not match metadata/); }, ); }); it("rejects working snapshots when a fake Git manifest changes after the patch", async () => { const root = await repository(); await withFakeGit( root, `const { existsSync, writeFileSync } = require("node:fs"); const { join } = require("node:path"); const args = process.argv.slice(2); const marker = join(process.cwd(), ".strata-race"); const out = (value) => process.stdout.write(value); if (args.includes("rev-parse")) out(args.includes("--show-toplevel") ? process.cwd() + "\\n" : "${"a".repeat(40)}\\n"); else if (args[0] === "ls-files") out(args.includes("--others") ? (existsSync(marker) ? "later.todo\\0" : "") : "before.txt\\0later.txt\\0"); else if (args[0] === "ls-tree") out("before.txt\\0later.txt\\0"); else if (args.includes("--name-status")) out(existsSync(marker) ? "M\\0later.txt\\0" : "M\\0before.txt\\0"); else if (args.includes("--patch")) { writeFileSync(marker, "changed"); out("diff --git a/before.txt b/before.txt\\nindex 1..2 100644\\n--- a/before.txt\\n+++ b/before.txt\\n@@ -1 +1 @@\\n-old\\n+new\\n"); }`, async () => { await expect( collectSnapshot(root, { kind: "working" }), ).rejects.toThrow(/working tree changed during snapshot capture/); }, ); }); it("excludes protected rename sides so the unprotected side remains reviewable", async () => { const root = await repository(); await withFakeGit( root, `const args = process.argv.slice(2); const out = (value) => process.stdout.write(value); const protectedPath = "pi/agent/auth.json"; if (args.includes("rev-parse")) out(args.includes("--show-toplevel") ? process.cwd() + "\\n" : "${"a".repeat(40)}\\n"); else if (args[0] === "ls-files") out(args.includes("--others") ? "" : "public.txt\\0" + protectedPath + "\\0"); else if (args[0] === "ls-tree") out("public.txt\\0" + protectedPath + "\\0"); else if (args.includes("--name-status")) { if (!args.includes(":(exclude,literal)" + protectedPath)) process.exitCode = 2; else out("A\\0public.txt\\0"); } else if (args.includes("--name-only")) out(protectedPath + "\\0"); else if (args.includes("--patch")) { if (!args.includes(":(exclude,literal)" + protectedPath)) process.exitCode = 2; else out("diff --git a/public.txt b/public.txt\\nnew file mode 100644\\nindex 0000000..1111111\\n--- /dev/null\\n+++ b/public.txt\\n@@ -0,0 +1 @@\\n+public\\n"); }`, async () => { const snapshot = await collectSnapshot(root, { kind: "working" }); expect(snapshot.hunks.map((hunk) => hunk.path)).toEqual(["public.txt"]); expect(snapshot.skipped).toEqual([ { path: "pi/agent/auth.json", reason: "protected path" }, ]); }, ); }); it("disables external diff commands and text conversion from repository config", async () => { const root = await repository(); await writeFile( join(root, ".gitattributes"), "external.txt diff=external\nconverted.tc diff=converted\n", ); await writeFile(join(root, "external.txt"), "old external\n"); await writeFile(join(root, "converted.tc"), "old conversion\n"); await commitAll(root, "initial"); await git( root, "config", "diff.external.command", "definitely-not-a-command", ); await git( root, "config", "diff.converted.textconv", "definitely-not-a-command", ); await writeFile(join(root, "external.txt"), "new external\n"); await writeFile(join(root, "converted.tc"), "new conversion\n"); const snapshot = await collectSnapshot(root, { kind: "working" }); expect(snapshot.hunks.map((hunk) => hunk.path)).toEqual([ "converted.tc", "external.txt", ]); }); it("ignores ambient Git repository redirection", async () => { const root = await repository(); await writeFile(join(root, "tracked.txt"), "old\n"); await commitAll(root, "initial"); await writeFile(join(root, "tracked.txt"), "new\n"); const previous = process.env.GIT_DIR; process.env.GIT_DIR = join(root, "missing-git-dir"); try { const snapshot = await collectSnapshot(root, { kind: "working" }); expect(snapshot.hunks.map((hunk) => hunk.path)).toEqual(["tracked.txt"]); } finally { if (previous === undefined) delete process.env.GIT_DIR; else process.env.GIT_DIR = previous; } }); it("refuses oversized patches instead of truncating", async () => { const root = await repository(); await writeFile(join(root, "large.txt"), "small\n"); await commitAll(root, "initial"); await writeFile( join(root, "large.txt"), "x".repeat(MAX_PATCH_BYTES + 8_192), ); await expect(collectSnapshot(root, { kind: "working" })).rejects.toThrow( /byte limit/, ); }); it("honors cancellation before collection", async () => { const controller = new AbortController(); controller.abort(new Error("cancelled by test")); await expect( collectSnapshot("unused", { kind: "working" }, controller.signal), ).rejects.toThrow("cancelled by test"); }); });