import { chmodSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createPiSession, type PiSession, } from "../../../test/fixture/create-pi-session.ts"; let session: PiSession | undefined; let fakeRtkDir: string | undefined; function rtkPath(): string { return fileURLToPath(new URL("../index.ts", import.meta.url)); } function rtkPathEnv(dir: string): string { return `${dir}${path.delimiter}${process.env["PATH"] ?? ""}`; } 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 () => { fakeRtkDir = installFakeRtk(); session = await createPiSession({ extensionPaths: [rtkPath()], env: { PATH: rtkPathEnv(fakeRtkDir) }, }); }); afterEach(async () => { await session?.dispose(); session = undefined; if (fakeRtkDir) rmSync(fakeRtkDir, { recursive: true, force: true }); fakeRtkDir = undefined; }); describe("rtk contract integration", () => { it("loads with no tools and no commands", () => { const [extension] = session?.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 fixture sandbox", async () => { const root = mkdtempSync(path.join(tmpdir(), "pi-ext-rtk-debug-")); const debug = await createPiSession({ cwd: root, extensionPaths: [rtkPath()], env: { PI_RTK_DEBUG: "1", PATH: rtkPathEnv(fakeRtkDir ?? ""), }, }); 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); if (!file) throw new Error("missing debug file"); 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 { await 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 session?.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 session?.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 session?.session.extensionRunner.emitToolCall(event as never); expect(event.input.command).toBe("rtk find . -name AGENTS.md"); }); });