Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/strata/__tests__/core.test.ts

Raw
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<string> {
	const { stdout } = await execFileAsync("git", args, {
		cwd,
		env: cleanGitEnvironment(),
		encoding: "utf8",
		maxBuffer: 2 * 1024 * 1024,
	});
	return stdout.trim();
}

async function repository(): Promise<string> {
	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<void>,
): Promise<void> {
	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<string> {
	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");
	});
});