import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestSession, type TestSession } from "../../../test/harness"; describe("mockup pi-test-harness", () => { let t: TestSession | undefined; afterEach(() => { t?.dispose(); t = undefined; }); it("contributes review context through a named prompt section", async () => { const extensionPath = path.resolve(import.meta.dirname, "../index.ts"); t = await createTestSession({ extensions: [extensionPath] }); t.session.sessionManager.appendMessage({ role: "toolResult", toolCallId: "mockup-call", toolName: "mockup", content: [{ type: "text", text: "accepted" }], details: { sessionId: t.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 t.session.extensionRunner.emitBeforeAgentStart( "prompt", undefined, { cwd: t.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-")); const original = { debug: process.env.PI_MOCKUP_DEBUG, state: process.env.XDG_STATE_HOME, }; try { process.env.PI_MOCKUP_DEBUG = "1"; process.env.XDG_STATE_HOME = path.join(root, "state"); vi.resetModules(); const { default: extension } = await import("../index.ts"); t = await createTestSession({ cwd: root, env: { PI_MOCKUP_DEBUG: "1", XDG_STATE_HOME: path.join(root, "state"), }, extensionFactories: [extension], }); await t.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); const definition = t.session.extensionRunner.extensions[0]?.tools.get( "mockup", )?.definition; if (!definition?.execute) throw new Error("missing mockup tool"); const base = t.session.extensionRunner.createContext(); 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 t.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); const events = readdirSync( path.join(root, "state", "pi-ext", "debug", "mockup"), ).flatMap((file) => readFileSync( path.join(root, "state", "pi-ext", "debug", "mockup", 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 { t?.dispose(); t = undefined; if (original.debug === undefined) delete process.env.PI_MOCKUP_DEBUG; else process.env.PI_MOCKUP_DEBUG = original.debug; if (original.state === undefined) delete process.env.XDG_STATE_HOME; else process.env.XDG_STATE_HOME = original.state; rmSync(root, { recursive: true, force: true }); } }); it("loads the tool and its authoring skill in the real Pi runtime", async () => { const extensionPath = path.resolve(import.meta.dirname, "../index.ts"); t = await createTestSession({ extensions: [extensionPath] }); expect(t.session.extensionRunner.extensions).toHaveLength(1); expect(t.session.extensionRunner.extensions[0].path).toBe(extensionPath); expect(t.session.getActiveToolNames()).toContain("mockup"); expect(t.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 = t.session.resourceLoader .getSkills() .skills.find((candidate) => candidate.name === "mockup-html"); expect(skill?.filePath).toBe(guidancePath); expect(t.session.systemPrompt).toContain("mockup-html"); expect(t.session.systemPrompt).toContain(guidancePath); expect(t.session.systemPrompt).not.toContain( "Rich interactions, scripts, and animations are allowed", ); }); });