Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/rtk/__tests__/harness.test.ts

Raw
import {
	chmodSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createTestSession, type TestSession } from "../../../test/harness";

let t: TestSession | undefined;
let oldPath = "";

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

function installFakeRtk(): string {
	const dir = mkdtempSync(path.join(tmpdir(), "pi-ext-rtk-"));
	const script = [
		"#!/usr/bin/env node",
		"const [, , sub, command] = process.argv;",
		"if (sub === '--version') { console.log('rtk 0.37.2'); process.exit(0); }",
		"if (sub === 'rewrite' && command === 'git status') { console.log('rtk git status'); process.exit(0); }",
		"if (sub === 'rewrite' && command === 'find . -name AGENTS.md') { console.log('rtk find . -name AGENTS.md'); process.exit(3); }",
		"if (sub === 'rewrite' && command === 'uptime') { process.exit(2); }",
		"process.exit(1);",
		"",
	].join("\n");
	const bin = path.join(dir, process.platform === "win32" ? "rtk.cmd" : "rtk");
	if (process.platform === "win32") {
		writeFileSync(
			bin,
			`@echo off\nnode "${path.join(dir, "rtk.js")}" %*\n`,
			"utf8",
		);
		writeFileSync(path.join(dir, "rtk.js"), script, "utf8");
	} else {
		writeFileSync(bin, script, "utf8");
		chmodSync(bin, 0o755);
	}
	return dir;
}

beforeEach(async () => {
	oldPath = process.env.PATH ?? "";
	process.env.PATH = `${installFakeRtk()}${path.delimiter}${oldPath}`;
	t = await createTestSession({ extensions: [rtkPath()] });
});

afterEach(() => {
	t?.dispose();
	t = undefined;
	process.env.PATH = oldPath;
});

describe("rtk harness integration", () => {
	it("loads with no tools and no commands", () => {
		const [extension] = t?.session.extensionRunner.extensions ?? [];
		expect(extension?.path).toBe(rtkPath());
		expect([...(extension?.tools.keys() ?? [])]).toEqual([]);
		expect([...(extension?.commands.keys() ?? [])]).toEqual([]);
	});

	it("writes its opt-in lifecycle trace inside the harness sandbox", async () => {
		const root = mkdtempSync(path.join(tmpdir(), "pi-ext-rtk-debug-"));
		const debug = await createTestSession({
			cwd: root,
			extensions: [rtkPath()],
			env: { PI_RTK_DEBUG: "1" },
		});
		try {
			const runner = debug.session.extensionRunner;
			await runner.emit({ type: "session_start", reason: "startup" });
			await runner.emitToolCall({
				type: "tool_call",
				toolName: "bash",
				toolCallId: "debug-rewrite",
				input: { command: "git status" },
			} as never);
			await runner.emitToolCall({
				type: "tool_call",
				toolName: "bash",
				toolCallId: "debug-fallback",
				input: { command: "ps aux" },
			} as never);
			await runner.emitToolCall({
				type: "tool_call",
				toolName: "bash",
				toolCallId: "debug-failure",
				input: { command: "uptime" },
			} as never);
			await runner.emit({ type: "session_shutdown", reason: "quit" });
			const stateHome = debug.env.XDG_STATE_HOME;
			if (!stateHome) throw new Error("missing sandbox state home");
			const directory = path.join(stateHome, "pi-ext", "debug", "rtk");
			const [file] = readdirSync(directory);
			const records = readFileSync(path.join(directory, file), "utf8")
				.trim()
				.split("\n")
				.map((line) => JSON.parse(line));
			expect(records.map((record) => record.event)).toEqual(
				expect.arrayContaining([
					"session.start",
					"session.shutdown",
					"rtk.rewrite",
				]),
			);
			expect(
				records
					.filter((record) => record.event === "rtk.rewrite")
					.map((record) => record.outcome),
			).toEqual(["rewritten", "fallback", "unchanged"]);
			expect(
				records.find(
					(record) =>
						record.event === "rtk.rewrite" && record.outcome === "unchanged",
				)?.reason,
			).toBe("unexpected_exit");
		} finally {
			debug.dispose();
			rmSync(root, { recursive: true, force: true });
		}
	});

	it("mutates bash tool calls before execution", async () => {
		const event = {
			type: "tool_call",
			toolName: "bash",
			toolCallId: "1",
			input: { command: "git status" },
		};
		await t?.session.extensionRunner.emitToolCall(event as never);
		expect(event.input.command).toBe("rtk git status");
	});

	it("mutates local fallback bash tool calls before execution", async () => {
		const event = {
			type: "tool_call",
			toolName: "bash",
			toolCallId: "2",
			input: { command: "ps aux" },
		};
		await t?.session.extensionRunner.emitToolCall(event as never);
		expect(event.input.command).toBe("rtk summary ps aux");
	});

	it("rewrites find commands without a compatibility probe", async () => {
		const event = {
			type: "tool_call",
			toolName: "bash",
			toolCallId: "3",
			input: { command: "find . -name AGENTS.md" },
		};
		await t?.session.extensionRunner.emitToolCall(event as never);
		expect(event.input.command).toBe("rtk find . -name AGENTS.md");
	});
});