Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/git-safe/__tests__/harness.test.ts

Raw
import {
	accessSync,
	chmodSync,
	constants,
	copyFileSync,
	mkdirSync,
	mkdtempSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { createBashTool, initTheme } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it } from "vitest";
import {
	createTestSession,
	type TestSession,
	withProcessEnv,
} from "../../../test/harness";

type BashEvent = {
	type: "tool_call";
	toolName: "bash";
	toolCallId: string;
	input: { command: string };
};

function gitSafePath(): string {
	return path.resolve(import.meta.dirname, "../index.ts");
}

function rtkPath(): string {
	return path.resolve(import.meta.dirname, "../../rtk/index.ts");
}

function bashShebang(): string {
	if (process.platform === "win32") return "#!/usr/bin/env bash";
	const candidates = [
		process.env.SHELL,
		...(process.env.PATH ?? "")
			.split(path.delimiter)
			.filter(Boolean)
			.map((directory) => path.join(directory, "bash")),
		"/bin/bash",
	];
	for (const candidate of candidates) {
		if (!candidate || path.basename(candidate) !== "bash") continue;
		try {
			accessSync(candidate, constants.X_OK);
			return `#!${candidate}`;
		} catch {}
	}
	throw new Error("bash executable not found");
}

function installFakeGit(root: string): { bin: string; executable: string } {
	const bin = path.join(root, "fake-bin");
	mkdirSync(bin);
	if (process.platform === "win32") {
		const gitExe = path.join(bin, "git.exe");
		copyFileSync(process.execPath, gitExe);
		const implementation = path.join(root, "fake-git.cjs");
		writeFileSync(
			implementation,
			[
				'const path = require("node:path");',
				"const command = path.basename(process.argv[1]);",
				"const args = process.argv.slice(2);",
				"if (command === 'config' && args.includes('--get')) process.exit(1);",
				"process.stdout.write('FAKE_GIT\\n');",
				"process.stdout.write(`ARGS:<${[command, ...args].join('><')}>\\n`);",
				"process.stdout.write(`EDITOR=<${process.env.GIT_EDITOR ?? ''}> SEQUENCE=<${process.env.GIT_SEQUENCE_EDITOR ?? ''}> MERGE=<${process.env.GIT_MERGE_AUTOEDIT ?? ''}>\\n`);",
				"",
			].join("\n"),
			"utf8",
		);
		for (const command of [
			"add",
			"am",
			"commit",
			"config",
			"grep",
			"merge",
			"note",
			"nv",
			"pull",
			"push",
			"rebase",
			"status",
			"var",
		]) {
			writeFileSync(
				path.join(root, command),
				`require(${JSON.stringify(implementation)});\n`,
				"utf8",
			);
		}
		return { bin, executable: gitExe };
	}

	const git = path.join(bin, "git");
	writeFileSync(
		git,
		[
			bashShebang(),
			"printf 'FAKE_GIT\\n'",
			"printf 'ARGS:'",
			"printf '<%s>' \"$@\"",
			'printf \'\\nEDITOR=<%s> SEQUENCE=<%s> MERGE=<%s>\\n\' "$GIT_EDITOR" "$GIT_SEQUENCE_EDITOR" "$GIT_MERGE_AUTOEDIT"',
			"",
		].join("\n"),
		"utf8",
	);
	chmodSync(git, 0o755);
	return { bin, executable: git };
}

function installFakeRtk(bin: string): void {
	const rtk = path.join(bin, "rtk");
	writeFileSync(
		rtk,
		[
			bashShebang(),
			"if [[ \"$1\" == '--version' ]]; then printf 'rtk 0.42.4\\n'; exit 0; fi",
			"if [[ \"$1\" == 'rewrite' && \"$2\" == 'git status' ]]; then printf 'rtk git status\\n'; exit 0; fi",
			'if [[ "$1" == \'git\' ]]; then shift; exec git "$@"; fi',
			"exit 1",
			"",
		].join("\n"),
		"utf8",
	);
	chmodSync(rtk, 0o755);
}

function bashEvent(command: string): BashEvent {
	return {
		type: "tool_call",
		toolName: "bash",
		toolCallId: crypto.randomUUID(),
		input: { command },
	};
}

function theme() {
	return {
		fg: (_style: string, text: string) => text,
		bold: (text: string) => text,
	} as any;
}

function render(component: { render(width: number): string[] }): string {
	return component.render(120).join("\n");
}

describe("git-safe pi-test-harness", () => {
	let t: TestSession | undefined;
	let root: string | undefined;

	afterEach(() => {
		t?.dispose();
		if (root) rmSync(root, { recursive: true, force: true });
		t = undefined;
		root = undefined;
	});

	async function startWithFakeGit(useRtk = false): Promise<void> {
		root = mkdtempSync(path.join(tmpdir(), "pi-git-safe-test-"));
		const installed = installFakeGit(root);
		if (useRtk) installFakeRtk(installed.bin);
		t = await createTestSession({
			cwd: root,
			env: { PATH: `${installed.bin}${path.delimiter}${process.env.PATH}` },
			extensions: useRtk ? [gitSafePath(), rtkPath()] : [gitSafePath()],
		});
	}

	async function execute(command: string) {
		if (!t) throw new Error("missing test session");
		const event = bashEvent(command);
		const blocked = await t.session.extensionRunner.emitToolCall(
			event as never,
		);
		if (blocked) return { blocked, output: "" };
		const tool = createBashTool(t.cwd);
		try {
			const result = await withProcessEnv(t.env, () =>
				tool.execute(
					event.toolCallId,
					event.input,
					new AbortController().signal,
					undefined,
				),
			);
			const output = result.content
				.flatMap((part) => (part.type === "text" ? [part.text] : []))
				.join("\n");
			return { blocked, output };
		} catch (error) {
			return {
				blocked,
				output: error instanceof Error ? error.message : String(error),
			};
		}
	}

	async function expectGitOutput(command: string): Promise<void> {
		const result = await execute(command);
		expect(result.blocked, command).toBeUndefined();
		expect(result.output, command).toContain("FAKE_GIT");
	}

	it("loads in real Pi extension runtime", async () => {
		t = await createTestSession({ extensions: [gitSafePath()] });

		expect(t.session.extensionRunner.extensions).toHaveLength(1);
		const tool =
			t.session.extensionRunner.extensions[0].tools.get(
				"git_clone_safe",
			).definition;
		expect(tool.description).toContain(
			"Clone an HTTPS or SSH Git repository into a safe subdirectory",
		);
		expect(tool.description).toContain(
			"canonical path, file/size/symlink counts, and warnings",
		);
		expect(tool.promptSnippet).toBe("Safely clone Git repositories.");
		expect(tool.promptGuidelines).toEqual([
			"Use git_clone_safe instead of raw git clone.",
		]);
		expect(t.session.systemPrompt).toContain(
			"Use git_clone_safe instead of raw git clone.",
		);
	});

	it("reveals clone warnings only when expanded", async () => {
		initTheme("default", false);
		t = await createTestSession({ extensions: [gitSafePath()] });
		const tool =
			t.session.extensionRunner.extensions[0].tools.get(
				"git_clone_safe",
			).definition;
		const result = {
			content: [{ type: "text", text: "cloned" }],
			details: {
				url: "https://example.com/owner/repo.git",
				path: "/tmp/owner/repo",
				fileCount: 2,
				totalSizeBytes: 128,
				symlinkCount: 0,
				symlinks: [],
				warnings: ["Submodules were not initialized"],
			},
		};
		const collapsed = render(
			tool.renderResult(result, { expanded: false }, theme()),
		);
		const expanded = render(
			tool.renderResult(result, { expanded: true }, theme()),
		);
		expect(collapsed).toContain("to expand");
		expect(collapsed).not.toContain("Submodules were not initialized");
		expect(expanded).toContain("Submodules were not initialized");
		expect(expanded).not.toContain("to expand");

		const error = {
			content: [
				{
					type: "text",
					text: "Clone failed\ncause\nnext step\nalternative\nEND-OF-DIAGNOSTIC",
				},
			],
			isError: true,
		};
		const collapsedError = render(
			tool.renderResult(error, { expanded: false }, theme()),
		);
		const expandedError = render(
			tool.renderResult(error, { expanded: true }, theme()),
		);
		expect(collapsedError).toContain("to expand");
		expect(collapsedError).not.toContain("END-OF-DIAGNOSTIC");
		expect(expandedError).toContain("END-OF-DIAGNOSTIC");

		const longError = {
			content: [
				{
					type: "text",
					text: `Clone failed: ${"diagnostic ".repeat(30)}END-OF-LONG-DIAGNOSTIC`,
				},
			],
			isError: true,
		};
		const collapsedLongError = render(
			tool.renderResult(longError, { expanded: false }, theme()),
		);
		const expandedLongError = render(
			tool.renderResult(longError, { expanded: true }, theme()),
		);
		expect(collapsedLongError).toContain("to expand");
		expect(collapsedLongError).not.toContain("END-OF-LONG-DIAGNOSTIC");
		expect(expandedLongError).toContain("END-OF-LONG-DIAGNOSTIC");
	});

	it("recovers a marked clone lazily when read", async () => {
		root = mkdtempSync(path.join(tmpdir(), "pi-git-safe-read-"));
		const clone = path.join(root, "owner", "repo");
		const file = path.join(clone, "README.md");
		mkdirSync(clone, { recursive: true });
		writeFileSync(path.join(clone, ".pi-git-safe"), "{}", "utf8");
		writeFileSync(file, "untrusted instructions", "utf8");
		t = await createTestSession({ cwd: root, extensions: [gitSafePath()] });

		const result = await t.session.extensionRunner.emitToolResult({
			type: "tool_result",
			toolName: "read",
			toolCallId: crypto.randomUUID(),
			input: { path: file },
			content: [{ type: "text", text: "untrusted instructions" }],
			isError: false,
		} as never);

		expect(result?.content[0]).toMatchObject({ type: "text" });
		expect(
			result?.content[0]?.type === "text" && result.content[0].text,
		).toContain("UNTRUSTED DATA");
		expect(
			result?.content[0]?.type === "text" && result.content[0].text,
		).toContain("untrusted instructions");
	});

	it("sets noninteractive editor variables while ordinary Git still runs", async () => {
		await startWithFakeGit();

		const result = await execute("git status");

		expect(result.blocked).toBeUndefined();
		expect(result.output).toContain("FAKE_GIT");
		expect(result.output).toContain("ARGS:<status>");
		expect(result.output).toContain("EDITOR=<true> SEQUENCE=<true> MERGE=<no>");
	});

	it("restores editor protection at the executable boundary", async () => {
		await startWithFakeGit();

		for (const command of [
			"GIT_EDITOR=vim git commit",
			"GIT_SEQUENCE_EDITOR=vim git rebase -i HEAD~1",
			"env -u GIT_EDITOR -u GIT_SEQUENCE_EDITOR -u GIT_MERGE_AUTOEDIT git commit",
			"GIT_EDITOR=vim command -p git commit",
		]) {
			const result = await execute(command);
			expect(result.output).toContain("FAKE_GIT");
			expect(result.output).toContain(
				"EDITOR=<true> SEQUENCE=<true> MERGE=<no>",
			);
		}
	});

	it("preserves editor queries without opening an editor", async () => {
		await startWithFakeGit();

		const result = await execute("git var GIT_EDITOR");
		expect(result.output).toContain("FAKE_GIT");
		expect(result.output).toContain("EDITOR=<> SEQUENCE=<> MERGE=<>");
	});

	it("does not trust an agent-supplied policy marker", async () => {
		await startWithFakeGit();

		await expectGitOutput(
			"# pi-git-safe-policy-v1\\ngit status".replace("\\n", "\n"),
		);
	});

	it("ignores caller-controlled executable overrides", async () => {
		await startWithFakeGit();
		if (!root) throw new Error("missing test root");
		const alternateNode = path.join(root, "alternate-node");
		writeFileSync(alternateNode, "#!/bin/sh\nprintf 'OVERRIDE\\n'\n", "utf8");
		chmodSync(alternateNode, 0o755);

		for (const command of [
			`PI_GIT_SAFE_REAL_GIT=${JSON.stringify(process.execPath)} git status`,
			`PI_GIT_SAFE_NODE=${JSON.stringify(alternateNode)} git status`,
		]) {
			const result = await execute(command);
			expect(result.output, command).toContain("FAKE_GIT");
			expect(result.output, command).not.toContain("OVERRIDE");
		}
	});

	it("keeps policy active after RTK rewrites Git", async () => {
		await startWithFakeGit(true);

		const ordinary = await execute("git status");
		expect(ordinary.output).toContain("FAKE_GIT");
		expect(ordinary.output).toContain("ARGS:<status>");
	});
});