repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/git-safe/__tests__/contract.test.ts
Rawimport { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { initTheme } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it } from "vitest";
import {
createPiSession,
type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
function gitSafePath(): string {
return fileURLToPath(new URL("../index.ts", import.meta.url));
}
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");
}
// Full ToolRenderContext is not part of the package's public type exports, so
// build the runtime shape the renderer receives. git_clone_safe's renderResult
// ignores this context, but the signature requires it.
function renderContext(expanded: boolean) {
return {
args: {},
toolCallId: "test",
invalidate: () => {},
lastComponent: undefined,
state: undefined,
cwd: process.cwd(),
executionStarted: true,
argsComplete: true,
isPartial: false,
expanded,
showImages: false,
isError: false,
durationMs: undefined,
outputPad: 0,
};
}
describe("git-safe owned fixture", () => {
let session: PiSession | undefined;
let root: string | undefined;
afterEach(async () => {
await session?.dispose();
if (root) rmSync(root, { recursive: true, force: true });
session = undefined;
root = undefined;
});
it("loads in real Pi extension runtime", async () => {
session = await createPiSession({ extensionPaths: [gitSafePath()] });
expect(session.extensions).toHaveLength(1);
const tool = session.extensions[0]!.tools.get("git_clone_safe")!.definition;
expect(tool.exposure).toBe("deferred");
expect(tool.annotations?.readOnlyHint).toBe(false);
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(session.session.getActiveToolNames()).not.toContain(
"git_clone_safe",
);
expect(session.session.systemPrompt).not.toContain(
"Use git_clone_safe instead of raw git clone.",
);
});
it("reveals clone warnings only when expanded", async () => {
initTheme("default", false);
session = await createPiSession({ extensionPaths: [gitSafePath()] });
const tool = session.extensions[0]!.tools.get("git_clone_safe")!.definition;
const result = {
content: [{ type: "text" as const, 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, isPartial: false },
theme(),
renderContext(false),
),
);
const expanded = render(
tool.renderResult!(
result,
{ expanded: true, isPartial: false },
theme(),
renderContext(true),
),
);
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" as const,
text: "Clone failed\ncause\nnext step\nalternative\nEND-OF-DIAGNOSTIC",
},
],
isError: true,
details: undefined,
};
const collapsedError = render(
tool.renderResult!(
error,
{ expanded: false, isPartial: false },
theme(),
renderContext(false),
),
);
const expandedError = render(
tool.renderResult!(
error,
{ expanded: true, isPartial: false },
theme(),
renderContext(true),
),
);
expect(collapsedError).toContain("to expand");
expect(collapsedError).not.toContain("END-OF-DIAGNOSTIC");
expect(expandedError).toContain("END-OF-DIAGNOSTIC");
const longError = {
content: [
{
type: "text" as const,
text: `Clone failed: ${"diagnostic ".repeat(30)}END-OF-LONG-DIAGNOSTIC`,
},
],
isError: true,
details: undefined,
};
const collapsedLongError = render(
tool.renderResult!(
longError,
{ expanded: false, isPartial: false },
theme(),
renderContext(false),
),
);
const expandedLongError = render(
tool.renderResult!(
longError,
{ expanded: true, isPartial: false },
theme(),
renderContext(true),
),
);
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");
session = await createPiSession({
cwd: root,
extensionPaths: [gitSafePath()],
});
const result = await session.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("leaves agent-issued shell commands untouched", async () => {
session = await createPiSession({ extensionPaths: [gitSafePath()] });
for (const toolName of ["bash", "nu", "powershell"]) {
const event = {
type: "tool_call",
toolName,
toolCallId: crypto.randomUUID(),
input: { command: "git status" },
};
const blocked = await session.session.extensionRunner.emitToolCall(
event as never,
);
expect(blocked, toolName).toBeUndefined();
expect(event.input.command, toolName).toBe("git status");
}
});
});