Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

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

Raw
import { 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");
		}
	});
});