import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import { createPiSession, type PiSession, } from "../../../test/fixture/create-pi-session.ts"; import { createMockupRuntime } from "../mockup-tool.ts"; function extensionPath(): string { return fileURLToPath(new URL("../index.ts", import.meta.url)); } describe("mockup pi-test-harness", () => { let session: PiSession | undefined; afterEach(async () => { await session?.dispose(); session = undefined; }); it("contributes review context through a named prompt section", async () => { session = await createPiSession({ extensionPaths: [extensionPath()] }); session.session.sessionManager.appendMessage({ role: "toolResult", toolCallId: "mockup-call", toolName: "mockup", content: [{ type: "text", text: "accepted" }], details: { sessionId: session.session.sessionManager.getSessionId(), chainId: "chain", iteration: 1, url: "http://127.0.0.1", history: [], contextCapsule: "review context", }, isError: false, timestamp: Date.now(), } as never); const result = await session.session.extensionRunner.emitBeforeAgentStart( { messageType: "user", prompt: "prompt" }, { cwd: session.cwd }, ); expect(result.systemPromptOptions.sections["mockup_context"]).toBe( "review context", ); expect(result.systemPromptOptions.forceSystemPrompt).toBeUndefined(); }); it("writes enabled lifecycle debug records in an isolated sandbox", async () => { const root = mkdtempSync(path.join(tmpdir(), "pi-ext-mockup-debug-")); try { session = await createPiSession({ cwd: root, env: { PI_MOCKUP_DEBUG: "1" }, extensionPaths: [extensionPath()], }); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); const definition = session.extensions[0]?.tools.get("mockup")?.definition; if (!definition?.execute) throw new Error("missing mockup tool"); expect(definition.exposure).toBe("model-only"); expect(definition.annotations?.readOnlyHint).toBe(false); expect( createMockupRuntime({ openBrowser: () => false }).tool.annotations ?.readOnlyHint, ).toBe(false); expect(definition.renderShell).toBeUndefined(); const base = session.session.extensionRunner.createToolContext( "mockup-debug", undefined, ); const ctx = Object.create(base) as typeof base; Object.defineProperties(ctx, { mode: { value: "tui" }, hasUI: { value: true }, ui: { value: { notify: () => undefined, select: async () => "Cancel", }, }, }); const cancelled = await definition.execute( "mockup-debug-cancel", { title: "Debug", question: "Continue?", options: [{ id: "one", label: "One", html: "

one

" }], }, undefined, undefined, ctx, ); expect(cancelled.details).toEqual({ kind: "cancelled" }); await expect( definition.execute("mockup-debug-fail", {}, undefined, undefined, ctx), ).rejects.toThrow(); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); const stateHome = session.env["XDG_STATE_HOME"]; if (!stateHome) throw new Error("missing sandbox state home"); const debugDirectory = path.join(stateHome, "pi-ext", "debug", "mockup"); const events = readdirSync(debugDirectory).flatMap((file) => readFileSync(path.join(debugDirectory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line)), ); expect(events).toEqual( expect.arrayContaining([ expect.objectContaining({ extension: "mockup", event: "tool.outcome", outcome: "cancelled", }), expect.objectContaining({ extension: "mockup", event: "tool.execute.finish", outcome: "cancelled", }), expect.objectContaining({ extension: "mockup", event: "tool.outcome", outcome: "failed", }), expect.objectContaining({ extension: "mockup", event: "tool.execute.error", outcome: "failed", }), ]), ); } finally { await session?.dispose(); session = undefined; rmSync(root, { recursive: true, force: true }); } }); it("loads the tool and its authoring skill in the real Pi runtime", async () => { const extPath = extensionPath(); session = await createPiSession({ extensionPaths: [extPath] }); expect(session.extensions).toHaveLength(1); expect(session.extensions[0]!.path).toBe(extPath); expect(session.session.getActiveToolNames()).toContain("mockup"); expect(session.session.systemPrompt).toContain( "- mockup: Preview HTML mockups in Pi or review them interactively in a browser", ); const guidancePath = path.resolve( import.meta.dirname, "../guidance/mockup-html/SKILL.md", ); const skill = session.session.resourceLoader .getSkills() .skills.find((candidate) => candidate.name === "mockup-html"); expect(skill?.filePath).toBe(guidancePath); expect(session.session.systemPrompt).toContain("mockup-html"); expect(session.session.systemPrompt).toContain(guidancePath); expect(session.session.systemPrompt).not.toContain( "Rich interactions, scripts, and animations are allowed", ); }); });