repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/git-safe/__tests__/harness.test.ts
Rawimport {
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>");
});
});