repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/mockup/__tests__/mockup-tool.test.ts
Rawimport assert from "node:assert";
import {
access,
mkdtemp,
readdir,
readFile,
rm,
stat,
utimes,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import {
type ExtensionContext,
initTheme,
} from "@earendil-works/pi-coding-agent";
import {
resetCapabilitiesCache,
setCapabilities,
} from "@earendil-works/pi-tui";
import { afterEach, beforeAll, describe, it, vi } from "vitest";
import type { MockupRequest, MockupResult } from "../mockup.ts";
import {
type MockupPreview,
type MockupPreviewManager,
PreviewCancelledError,
} from "../mockup-preview.ts";
import {
createMockupRuntime,
formatContextCapsule,
type MockupToolDetails,
mockupCacheRoot,
persistMockupIteration,
pruneMockupCache,
} from "../mockup-tool.ts";
const request: MockupRequest = {
title: "Layout",
question: "Which layout?",
options: [
{ id: "dense", label: "Dense", html: "<section>dense</section>" },
{ id: "calm", label: "Calm", html: "<section>calm</section>" },
],
};
const result: MockupResult = {
action: "iterate",
selected: ["dense"],
notes: "keep density",
annotations: [{ optionId: "dense", x: 0.25, y: 0.5, note: "tighten" }],
review: {
submittedAt: "2026-01-02T03:04:05.000Z",
userAgent: "test-browser/1",
viewport: { width: 1280, height: 720, devicePixelRatio: 1 },
},
};
const roots: string[] = [];
beforeAll(() => initTheme("default", false));
afterEach(async () => {
await Promise.all(
roots.splice(0).map((root) => rm(root, { recursive: true })),
);
});
async function temporaryRoot(): Promise<string> {
const root = await mkdtemp(path.join(tmpdir(), "pi-mockup-test-"));
roots.push(root);
return root;
}
async function exists(file: string): Promise<boolean> {
try {
await access(file);
return true;
} catch {
return false;
}
}
async function size(root: string): Promise<number> {
let bytes = 0;
for (const entry of await readdir(root, { withFileTypes: true })) {
const child = path.join(root, entry.name);
if (entry.isDirectory()) bytes += await size(child);
else if (entry.isFile()) bytes += (await stat(child)).size;
}
return bytes;
}
function plainTheme() {
return {
fg: (_style: string, text: string) => text,
bold: (text: string) => text,
} as never;
}
function rendered(component: { render(width: number): string[] }): string {
return component.render(120).join("\n");
}
function renderContext(
expanded: boolean,
lastComponent?: unknown,
state: Partial<{
isPartial: boolean;
argsComplete: boolean;
executionStarted: boolean;
}> = {},
) {
return {
expanded,
lastComponent,
isError: false,
isPartial: false,
argsComplete: true,
executionStarted: true,
...state,
} as never;
}
describe("mockup tool contract", () => {
it("keeps concise prompt metadata while guidance lives in a skill", async () => {
const tool = createMockupRuntime({ openBrowser: () => false }).tool;
const guidancePath = path.resolve(
import.meta.dirname,
"../guidance/mockup-html/SKILL.md",
);
assert.strictEqual(tool.label, "mockup");
assert.strictEqual(
tool.promptSnippet,
"Preview HTML mockups in Pi or review them interactively in a browser",
);
assert.notStrictEqual(tool.promptSnippet, tool.description);
assert.deepStrictEqual(tool.promptGuidelines ?? [], []);
assert.match(await readFile(guidancePath, "utf8"), /name: mockup-html/);
assert.strictEqual(tool.parameters.properties.options.minItems, 1);
assert.strictEqual(tool.parameters.properties.options.maxItems, 4);
assert.strictEqual(
tool.parameters.properties.options.items.properties.html.description,
"HTML fragment; scripts are allowed.",
);
});
it("renders a compact call and reserves option details for expansion", () => {
const tool = createMockupRuntime({ openBrowser: () => false }).tool;
assert.ok(tool.renderCall);
const collapsedComponent = tool.renderCall(
request,
plainTheme(),
renderContext(false),
);
const collapsed = rendered(collapsedComponent);
const expandedComponent = tool.renderCall(
request,
plainTheme(),
renderContext(true, collapsedComponent),
);
const expanded = rendered(expandedComponent);
assert.strictEqual(expandedComponent, collapsedComponent);
assert.match(collapsed, /^mockup Layout · 2 options/);
assert.match(collapsed, /to expand/);
assert.doesNotMatch(collapsed, /Which layout\?/);
assert.match(expanded, /Which layout\?/);
assert.match(expanded, /dense Dense/);
assert.match(expanded, /calm Calm/);
assert.doesNotMatch(expanded, /<section>/);
const generating = rendered(
tool.renderCall(
request,
plainTheme(),
renderContext(false, undefined, {
isPartial: true,
argsComplete: false,
executionStarted: false,
}),
),
);
assert.match(generating, /generating HTML · \d+ chars/);
const queued = rendered(
tool.renderCall(
request,
plainTheme(),
renderContext(false, undefined, {
isPartial: true,
argsComplete: true,
executionStarted: false,
}),
),
);
assert.match(queued, /waiting to run/);
const restored = rendered(
tool.renderCall(
request,
plainTheme(),
renderContext(false, undefined, {
isPartial: false,
argsComplete: false,
executionStarted: false,
}),
),
);
assert.doesNotMatch(restored, /generating|waiting to run/);
});
it("previews inside Pi without putting frame bytes in tool results", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
try {
let selected = 0;
let startedWith: MockupRequest | undefined;
const preview: MockupPreview = {
labels: request.options.map((option) => option.label),
done: Promise.resolve(),
current: () => [
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
],
cropped: () => [false, true],
expired: () => false,
version: () => 0,
subscribe: () => () => undefined,
close: () => undefined,
};
const manager: MockupPreviewManager = {
start: async (_id, value) => {
startedWith = value;
return preview;
},
get: () => preview,
close: async () => undefined,
};
const ctx = {
mode: "tui",
ui: {
select: async () => {
selected++;
return "Show previews in Pi";
},
notify: () => undefined,
},
sessionManager: {
getSessionId: () => "preview-session",
getBranch: () => [],
},
} as unknown as ExtensionContext;
const runtime = createMockupRuntime({ previewManager: manager });
const updates: Array<{ details?: unknown }> = [];
const output = await runtime.tool.execute(
"preview-call",
request,
undefined,
(update) => updates.push(update),
ctx,
);
assert.strictEqual(selected, 1);
assert.deepStrictEqual(startedWith, { ...request, multi: false });
assert.deepStrictEqual(
updates.map((update) => update.details),
[
{ kind: "preview", status: "starting" },
{ kind: "preview", status: "running" },
],
);
assert.deepStrictEqual(output.details, {
kind: "preview",
status: "complete",
});
assert.doesNotMatch(JSON.stringify(output), /data:image|iVBOR/);
assert.strictEqual(output.terminate, true);
assert.ok(runtime.tool.renderResult);
const hidden = runtime.tool.renderResult(
output,
{ expanded: false, isPartial: false },
plainTheme(),
{
...renderContext(false),
toolCallId: "preview-call",
showImages: false,
invalidate: () => undefined,
} as never,
);
assert.match(rendered(hidden), /Inline image previews are disabled/);
const visible = runtime.tool.renderResult(
output,
{ expanded: false, isPartial: false },
plainTheme(),
{
...renderContext(false, hidden),
toolCallId: "preview-call",
showImages: true,
invalidate: () => undefined,
} as never,
);
assert.strictEqual(visible, hidden);
assert.match(rendered(visible), /\x1b_G/);
assert.match(rendered(visible), /Preview cropped · use browser review/);
const cancelled = runtime.tool.renderResult(
{
content: [{ type: "text", text: "cancelled" }],
details: { kind: "cancelled" },
},
{ expanded: false, isPartial: false },
plainTheme(),
{
...renderContext(false, visible),
toolCallId: "preview-call",
} as never,
);
assert.match(rendered(cancelled), /Mockup cancelled/);
} finally {
resetCapabilitiesCache();
}
});
it("does not offer browser fallback when preview startup is cancelled", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
try {
let selections = 0;
let notifications = 0;
const manager: MockupPreviewManager = {
start: async () => {
throw new PreviewCancelledError();
},
get: () => undefined,
close: async () => undefined,
};
const ctx = {
mode: "tui",
ui: {
select: async () => {
selections++;
return "Show previews in Pi";
},
notify: () => notifications++,
},
sessionManager: {
getSessionId: () => "cancelled-preview-session",
getBranch: () => [],
},
} as unknown as ExtensionContext;
const output = await createMockupRuntime({
previewManager: manager,
}).tool.execute("cancelled-preview", request, undefined, undefined, ctx);
assert.strictEqual(selections, 1);
assert.strictEqual(notifications, 0);
assert.deepStrictEqual(output.details, { kind: "cancelled" });
} finally {
resetCapabilitiesCache();
}
});
it("keeps feedback compact until expanded and renders errors", () => {
const tool = createMockupRuntime({ openBrowser: () => false }).tool;
assert.ok(tool.renderResult);
const toolResult = {
content: [{ type: "text" as const, text: "structured result" }],
details: {
...result,
sessionId: "session",
chainId: "chain",
iteration: 1,
url: "http://127.0.0.1:1234/?k=secret",
cacheManifest: "/cache/manifest.json",
history: [],
contextCapsule: "capsule",
},
};
const collapsedComponent = tool.renderResult(
toolResult,
{ expanded: false, isPartial: false },
plainTheme(),
renderContext(false),
);
const collapsed = rendered(collapsedComponent);
const expandedComponent = tool.renderResult(
toolResult,
{ expanded: true, isPartial: false },
plainTheme(),
renderContext(true, collapsedComponent),
);
const expanded = rendered(expandedComponent);
assert.strictEqual(expandedComponent, collapsedComponent);
assert.match(collapsed, /^↻ iterate · selected dense · 1 annotation/);
assert.match(collapsed, /to expand/);
assert.doesNotMatch(collapsed, /keep density|tighten|cache/);
assert.match(expanded, /notes: keep density/);
assert.match(expanded, /dense @ 25%,50% tighten/);
assert.match(expanded, /http:\/\/127\.0\.0\.1:1234\/\?k=secret/);
assert.match(expanded, /cache: \/cache\/manifest\.json/);
const waiting = rendered(
tool.renderResult(
{
content: [{ type: "text", text: "Waiting for browser feedback" }],
details: {
kind: "waiting",
status: "waiting",
url: "http://127.0.0.1:4321/?k=secret",
iteration: 2,
browserFailed: true,
},
},
{ expanded: false, isPartial: true },
plainTheme(),
renderContext(false),
),
);
assert.match(waiting, /Browser did not open/);
assert.match(waiting, /http:\/\/127\.0\.0\.1:4321\/\?k=secret/);
const accepted = rendered(
tool.renderResult(
{
...toolResult,
details: {
...toolResult.details,
action: "accept",
selected: [],
annotations: [],
},
},
{ expanded: false, isPartial: false },
plainTheme(),
renderContext(false),
),
);
assert.match(accepted, /^✓ accepted · selected none · 0 annotations/);
const errorResult = {
content: [{ type: "text", text: "browser failed\nEND-BROWSER-DETAILS" }],
details: {},
};
const error = rendered(
tool.renderResult(
errorResult,
{ expanded: false, isPartial: false },
plainTheme(),
{ ...renderContext(false), isError: true } as never,
),
);
const expandedError = rendered(
tool.renderResult(
errorResult,
{ expanded: true, isPartial: false },
plainTheme(),
{ ...renderContext(true), isError: true } as never,
),
);
assert.match(error, /^browser failed/);
assert.match(error, /to expand/);
assert.doesNotMatch(error, /END-BROWSER-DETAILS/);
assert.match(expandedError, /END-BROWSER-DETAILS/);
});
});
describe("mockup runtime lifecycle", () => {
it("pins active browser iterations and prompts again after abandonment", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
const root = await temporaryRoot();
const branch: unknown[] = [];
const opened: string[] = [];
const notifications: string[] = [];
let selections = 0;
const ctx = {
mode: "tui",
hasUI: true,
isIdle: () => false,
abort: () => undefined,
ui: {
notify: (message: string) => notifications.push(message),
select: async () => {
selections++;
return "Open interactive browser";
},
},
sessionManager: {
getSessionId: () => "session",
getSessionFile: () => undefined,
getBranch: () => branch,
},
} as unknown as ExtensionContext;
const runtime = createMockupRuntime({
cacheRoot: root,
openBrowser: () => true,
browserOpener: async (url) => {
opened.push(url);
return true;
},
});
const updates: Array<{ details?: unknown }> = [];
const firstExecution = runtime.tool.execute(
"one",
request,
undefined,
(update) => updates.push(update),
ctx,
);
await vi.waitFor(() => assert.ok(updates.at(-1)?.details));
const firstProgress = updates.at(-1)?.details as
| { url?: string }
| undefined;
assert.ok(firstProgress?.url);
await submit(firstProgress.url, "iterate");
const first = await firstExecution;
const firstDetails = first.details as MockupToolDetails;
assert.strictEqual(firstDetails.url, firstProgress.url);
assert.deepStrictEqual(
updates.map(
(update) => (update.details as { status?: string } | undefined)?.status,
),
["opening", "waiting"],
);
const measuredProgress = updates.at(-1)?.details as
| { timings?: { serverMs?: number; openerMs?: number } }
| undefined;
assert.strictEqual(typeof measuredProgress?.timings?.serverMs, "number");
assert.strictEqual(typeof measuredProgress?.timings?.openerMs, "number");
assert.match(
(first.content[0] as { text: string }).text,
/Apply the feedback and call mockup again/,
);
branch.push({
type: "message",
message: {
role: "toolResult",
toolName: "mockup",
details: firstDetails,
},
});
updates.length = 0;
const secondExecution = runtime.tool.execute(
"two",
request,
undefined,
(update) => updates.push(update),
ctx,
);
await vi.waitFor(() => assert.ok(updates.at(-1)?.details));
const secondProgress = updates.at(-1)?.details as
| { url?: string }
| undefined;
assert.ok(secondProgress?.url);
assert.strictEqual(secondProgress.url, firstProgress.url);
await submit(secondProgress.url, "iterate");
const second = await secondExecution;
const secondDetails = second.details as MockupToolDetails;
branch.push({
type: "message",
message: {
role: "toolResult",
toolName: "mockup",
details: secondDetails,
},
});
assert.deepStrictEqual(opened, [firstProgress.url, secondProgress.url]);
runtime.agentSettled(ctx);
updates.length = 0;
const thirdExecution = runtime.tool.execute(
"three",
request,
undefined,
(update) => updates.push(update),
ctx,
);
await vi.waitFor(() => assert.ok(updates.at(-1)?.details));
const thirdProgress = updates.at(-1)?.details as
| { url?: string }
| undefined;
assert.ok(thirdProgress?.url);
assert.notStrictEqual(thirdProgress.url, secondProgress.url);
await submit(thirdProgress.url, "accept");
const third = await thirdExecution;
const thirdDetails = third.details as MockupToolDetails;
assert.strictEqual(thirdDetails.chainId, firstDetails.chainId);
assert.strictEqual(thirdDetails.iteration, 3);
assert.deepStrictEqual(opened, [
firstProgress.url,
secondProgress.url,
thirdProgress.url,
]);
assert.strictEqual(selections, 2);
assert.deepStrictEqual(notifications, []);
resetCapabilitiesCache();
});
});
async function submit(
url: string,
action: MockupResult["action"],
): Promise<void> {
const response = await fetch(new URL("/api/submit", url), {
method: "POST",
headers: {
Authorization: `Bearer ${new URL(url).searchParams.get("k")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action,
selected: [],
notes: "",
annotations: [],
client: {
userAgent: "mockup-tool-test/1",
viewport: { width: 1280, height: 720, devicePixelRatio: 1 },
},
}),
});
assert.strictEqual(response.status, 200);
}
describe("mockup context cache", () => {
it("uses the native OS cache location", () => {
assert.strictEqual(
mockupCacheRoot("linux", { XDG_CACHE_HOME: "/cache" }, "/home/o"),
path.join("/cache", "pi", "mockups"),
);
assert.strictEqual(
mockupCacheRoot(
"win32",
{ LOCALAPPDATA: "C:\\Users\\o\\AppData\\Local" },
"C:\\Users\\o",
),
path.join("C:\\Users\\o\\AppData\\Local", "pi", "cache", "mockups"),
);
assert.strictEqual(
mockupCacheRoot("darwin", {}, "/Users/o"),
path.join("/Users/o", "Library", "Caches", "pi", "mockups"),
);
});
it("persists exact fragments, request, feedback, metadata, and lineage", async () => {
const root = await temporaryRoot();
const sessionFile = path.join(root, "session.jsonl");
await writeFile(sessionFile, "");
const first = await persistMockupIteration(root, {
sessionId: "session/one",
sessionFile,
chainId: "chain-one",
iteration: 1,
toolCallId: "tool/one",
request,
result,
});
const second = await persistMockupIteration(root, {
sessionId: "session/one",
sessionFile,
chainId: "chain-one",
iteration: 2,
toolCallId: "tool-two",
request,
result: { ...result, action: "accept", selected: [] },
previousManifest: first,
});
assert.strictEqual(
await readFile(path.join(path.dirname(first), "dense.html"), "utf8"),
request.options[0].html,
);
assert.deepStrictEqual(
JSON.parse(
await readFile(path.join(path.dirname(first), "request.json"), "utf8"),
),
request,
);
assert.deepStrictEqual(
JSON.parse(
await readFile(path.join(path.dirname(first), "feedback.json"), "utf8"),
),
result,
);
const manifest = JSON.parse(await readFile(second, "utf8"));
assert.strictEqual(manifest.previousManifest, first);
assert.strictEqual(manifest.options[0].html, "dense.html");
});
it("formats current feedback first and preserves earlier iteration context", () => {
const capsule = formatContextCapsule("chain", [
{
iteration: 1,
question: request.question,
options: request.options.map(({ id, label }) => ({ id, label })),
action: "iterate",
selected: ["dense"],
notes: "keep density",
annotations: result.annotations,
cacheManifest: "/cache/one/manifest.json",
},
{
iteration: 2,
question: "Now?",
options: request.options.map(({ id, label }) => ({ id, label })),
action: "accept",
selected: [],
notes: "none selected intentionally",
annotations: [],
cacheManifest: "/cache/two/manifest.json",
},
]);
assert.ok(
capsule.indexOf("Current: iteration 2, accept") <
capsule.indexOf("Earlier iterations"),
);
assert.match(capsule, /Selected: none/);
assert.match(capsule, /never infer rejection from non-selection/);
assert.match(
capsule,
/1 iterate; selected dense \(Dense\); notes: keep density/,
);
assert.match(capsule, /annotation dense: tighten/);
});
it("injects cached context only into its owning session", async () => {
const root = await temporaryRoot();
const capsule = "owned mockup context";
const details = {
...result,
sessionId: "owner",
chainId: "chain",
iteration: 1,
url: "http://127.0.0.1:1234/?k=secret",
history: [],
contextCapsule: capsule,
};
const context = (sessionId: string) =>
({
isIdle: () => true,
sessionManager: {
getSessionId: () => sessionId,
getBranch: () => [
{
type: "message",
message: {
role: "toolResult",
toolName: "mockup",
details,
},
},
],
},
}) as unknown as ExtensionContext;
const runtime = createMockupRuntime({
cacheRoot: root,
openBrowser: () => false,
});
assert.strictEqual(runtime.contextCapsule(context("owner")), capsule);
assert.strictEqual(
runtime.contextCapsule(context("new-session")),
undefined,
);
});
it("removes orphaned and expired entries while protecting the current session", async () => {
const root = await temporaryRoot();
const sessionFile = path.join(root, "live.jsonl");
await writeFile(sessionFile, "");
const live = await persistMockupIteration(root, {
sessionId: "live",
sessionFile,
chainId: "chain",
iteration: 1,
toolCallId: "one",
request,
result,
});
const orphan = await persistMockupIteration(root, {
sessionId: "orphan",
sessionFile: path.join(root, "missing.jsonl"),
chainId: "chain",
iteration: 1,
toolCallId: "one",
request,
result,
});
const old = new Date(Date.now() - 10_000);
await utimes(path.dirname(live), old, old);
await pruneMockupCache(root, "live", Date.now(), {
ttlMs: 1,
maxBytes: Number.MAX_SAFE_INTEGER,
});
assert.strictEqual(await exists(live), true);
assert.strictEqual(await exists(orphan), false);
await pruneMockupCache(root, undefined, Date.now(), {
ttlMs: 1,
maxBytes: Number.MAX_SAFE_INTEGER,
});
assert.strictEqual(await exists(live), false);
});
it("evicts least-recently-used iterations above the size cap", async () => {
const root = await temporaryRoot();
const sessionFile = path.join(root, "live.jsonl");
await writeFile(sessionFile, "");
const oldest = await persistMockupIteration(root, {
sessionId: "live",
sessionFile,
chainId: "chain",
iteration: 1,
toolCallId: "old",
request,
result,
});
const newest = await persistMockupIteration(root, {
sessionId: "live",
sessionFile,
chainId: "chain",
iteration: 2,
toolCallId: "new",
request,
result,
});
await utimes(path.dirname(oldest), new Date(1000), new Date(1000));
await utimes(path.dirname(newest), new Date(2000), new Date(2000));
const newestBytes = await size(path.dirname(newest));
await pruneMockupCache(root, undefined, Date.now(), {
ttlMs: Number.MAX_SAFE_INTEGER,
maxBytes: newestBytes,
});
assert.strictEqual(await exists(oldest), false);
assert.strictEqual(await exists(newest), true);
});
});