Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/mockup/__tests__/harness.test.ts

Raw
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: "<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",
		);
	});
});