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