Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/mockup/__tests__/contract.test.ts

Raw
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: "<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 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("<name>mockup-html</name>");
		expect(session.session.systemPrompt).toContain(guidancePath);
		expect(session.session.systemPrompt).not.toContain(
			"Rich interactions, scripts, and animations are allowed",
		);
	});
});