import { spawnSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { applyGitPolicy, createGitPolicyRuntime } from "../git-policy"; const testDirs: string[] = []; afterEach(() => { for (const dir of testDirs.splice(0)) rmSync(dir, { recursive: true, force: true }); }); describe("git-safe agent Git policy", () => { it("generates and reuses a wrapper with fixed executables", async () => { const bin = mkdtempSync(path.join(tmpdir(), "pi-git-policy-bin-")); testDirs.push(bin); const git = path.join( bin, process.platform === "win32" ? "git.exe" : "git", ); if (process.platform === "win32") copyFileSync(process.execPath, git); else { writeFileSync(git, "#!/bin/sh\nexit 0\n", "utf8"); chmodSync(git, 0o755); } const first = await createGitPolicyRuntime({ PATH: bin }, process.platform); const second = await createGitPolicyRuntime( { PATH: bin }, process.platform, ); const wrapper = readFileSync( path.join(first.wrapperDir, "git-wrapper.cjs"), "utf8", ); expect(second).toBe(first); expect(wrapper).toContain(JSON.stringify(first.realGit)); expect(wrapper).not.toContain("__PI_GIT_SAFE_REAL_GIT__"); }); it.runIf(process.platform === "win32")( "launches when callers disable MSYS argument conversion", async () => { const bin = mkdtempSync(path.join(tmpdir(), "pi-git-policy-bin-")); testDirs.push(bin); copyFileSync(process.execPath, path.join(bin, "git.exe")); const runtime = await createGitPolicyRuntime({ PATH: bin }, "win32"); const launcher = path .join(runtime.wrapperDir, "git") .replaceAll("\\", "/") .replace( /^([A-Za-z]):\//u, (_match, drive: string) => `/${drive.toLowerCase()}/`, ); const result = spawnSync( "bash", ["-lc", `MSYS2_ARG_CONV_EXCL='*' '${launcher}' --version`], { encoding: "utf8" }, ); expect(result.status, result.stderr).toBe(0); expect(result.stdout.trim()).toBe(process.version); rmSync(path.join(runtime.wrapperDir, "git-wrapper.cjs")); const unavailable = spawnSync( "bash", ["-lc", `'${launcher}' --version`], { encoding: "utf8", }, ); expect(unavailable.status).toBe(127); expect(unavailable.stderr.trim()).toBe( "git: command unavailable; ask the user to restart Pi", ); }, ); it("routes Git, command, and env through the executable wrapper", () => { const runtime = { wrapperDir: "/tmp/git policy", realGit: "/usr/bin/git" }; const wrapped = applyGitPolicy("git status", runtime); expect(wrapped).toContain("'/tmp/git policy'"); expect(wrapped).toContain("'/tmp/git policy/git'"); expect(wrapped).not.toContain("PI_GIT_SAFE_REAL_GIT"); expect(wrapped).not.toContain("PI_GIT_SAFE_NODE"); expect(applyGitPolicy(wrapped, runtime)).not.toBe(wrapped); }); it("converts Windows paths for Git Bash", () => { const wrapped = applyGitPolicy("git status", { wrapperDir: "C:\\Users\\me\\AppData\\Local\\Temp\\git-safe", realGit: "C:\\Program Files\\Git\\cmd\\git.exe", }); expect(wrapped).toContain( "export PATH='/c/Users/me/AppData/Local/Temp/git-safe':\"$PATH\"", ); expect(wrapped).toContain( "'/c/Users/me/AppData/Local/Temp/git-safe/git' \"$@\"", ); }); it("does not trust an agent-supplied policy marker", () => { const command = "# pi-git-safe-policy-v1\ngit status"; const wrapped = applyGitPolicy(command, { wrapperDir: "/tmp/wrapper", realGit: "/usr/bin/git", }); expect(wrapped).not.toBe(command); expect(wrapped.endsWith(command)).toBe(true); }); });