Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/handoff/__tests__/contract.test.ts

Raw
import { fileURLToPath } from "node:url";
import { SessionManager } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
	createPiSession,
	type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
import { getHandoffMessages, SYSTEM_PROMPT } from "../core.ts";

function extensionPath(): string {
	return fileURLToPath(new URL("../index.ts", import.meta.url));
}

describe("handoff contract", () => {
	let session: PiSession | undefined;

	afterEach(async () => {
		await session?.dispose();
		session = undefined;
		vi.restoreAllMocks();
	});

	it("registers only /handoff and no tools", async () => {
		session = await createPiSession({ extensionPaths: [extensionPath()] });
		expect(session.errors).toEqual([]);
		const extension = session.extensions[0]!;
		expect(extension.path).toBe(extensionPath());
		expect([...extension.commands.keys()]).toEqual(["handoff"]);
		expect([...extension.tools.keys()]).toEqual([]);
	});

	it("refuses to run outside the TUI", async () => {
		session = await createPiSession({ extensionPaths: [extensionPath()] });
		const ctx = session.session.extensionRunner.createContext();
		const notify = vi.spyOn(ctx.ui, "notify").mockImplementation(() => {});
		await session.extensions[0]!.commands.get("handoff")!.handler(
			"ship it",
			ctx as never,
		);
		expect(notify).toHaveBeenCalledWith(
			"handoff requires interactive mode",
			"error",
		);
	});

	it("honors a compaction entry when gathering branch messages", () => {
		const manager = SessionManager.inMemory("/tmp");
		manager.appendMessage({
			role: "user",
			content: "old request",
			timestamp: 1,
		});
		manager.appendMessage({
			role: "assistant",
			content: [{ type: "text", text: "old answer" }],
			api: "openai-completions",
			provider: "faux",
			model: "faux-model",
			usage: {
				input: 0,
				output: 0,
				cacheRead: 0,
				cacheWrite: 0,
				totalTokens: 0,
				cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
			},
			stopReason: "stop",
			timestamp: 2,
		});
		const kept = manager.appendMessage({
			role: "user",
			content: "kept request",
			timestamp: 3,
		});
		manager.appendCompaction("summary of old work", kept, 1_000);
		manager.appendMessage({
			role: "user",
			content: "after compaction",
			timestamp: 4,
		});

		const messages = getHandoffMessages(manager.getBranch());
		expect(messages.map((m) => m.role)).toEqual([
			"compactionSummary",
			"user",
			"user",
		]);
		expect(JSON.stringify(messages)).toContain("summary of old work");
		expect(JSON.stringify(messages)).toContain("kept request");
		expect(JSON.stringify(messages)).not.toContain("old answer");
	});

	it("maps every entry without compaction", () => {
		const manager = SessionManager.inMemory("/tmp");
		manager.appendMessage({ role: "user", content: "one", timestamp: 1 });
		manager.appendMessage({ role: "user", content: "two", timestamp: 2 });
		expect(getHandoffMessages(manager.getBranch())).toHaveLength(2);
	});

	it("instructs the generator about artifacts, redaction, and skills", () => {
		expect(SYSTEM_PROMPT).toContain("by file path, commit hash, or URL");
		expect(SYSTEM_PROMPT).toContain("[REDACTED]");
		expect(SYSTEM_PROMPT).toContain("## Suggested skills");
	});
});