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