repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/mockup/__tests__/harness.test.ts
Rawimport { 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: "<p>one</p>" }],
},
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("<name>mockup-html</name>");
expect(t.session.systemPrompt).toContain(guidancePath);
expect(t.session.systemPrompt).not.toContain(
"Rich interactions, scripts, and animations are allowed",
);
});
});