import { describe, it, expect, vi, beforeEach } from "vitest"; import { mkdtempSync, writeFileSync, existsSync, unlinkSync } from "fs"; import { join } from "path"; import { tmpdir, homedir } from "os"; vi.mock("@mariozechner/pi-coding-agent", () => ({ ExtensionAPI: class {}, })); vi.mock("typebox", () => ({ Type: { Object: (props: unknown) => props, Optional: (prop: unknown) => prop, String: (opts: unknown) => ({ type: "string", ...(opts as object) }), }, })); import extension from "../pi-extension.js"; const CACHE_FILE = join( homedir(), ".cache", "pi-project-map", "llm-cache.json", ); function clearCache() { if (existsSync(CACHE_FILE)) unlinkSync(CACHE_FILE); } describe("pi-extension", () => { let registeredTools: Record = {}; let registeredEvents: Record = {}; let mockCtx: any; let mockNotify: ReturnType; beforeEach(() => { clearCache(); registeredTools = {}; registeredEvents = {}; mockNotify = vi.fn(); mockCtx = { cwd: "/home/project", modelRegistry: {}, ui: { notify: mockNotify }, }; const mockPi = { registerTool: vi.fn((tool: any) => { registeredTools[tool.name] = tool; }), on: vi.fn((event: string, handler: any) => { registeredEvents[event] = handler; }), }; extension(mockPi); }); describe("tool registration", () => { it("registers 4 tools", () => { expect(Object.keys(registeredTools)).toHaveLength(4); expect(registeredTools).toHaveProperty("project_map_init"); expect(registeredTools).toHaveProperty("project_map_patch"); expect(registeredTools).toHaveProperty("project_map_validate"); expect(registeredTools).toHaveProperty("project_map_reinit"); }); it("registers session_start and before_agent_start events", () => { expect(registeredEvents).toHaveProperty("session_start"); expect(registeredEvents).toHaveProperty("before_agent_start"); }); }); describe("project_map_init tool", () => { it("returns error when Pi LLM is not accessible", async () => { // Create a temp dir with a file so initProject tries to use LLM const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); writeFileSync(join(dir, "test.ts"), `export const x = ${Date.now()};`); mockCtx.cwd = dir; const tool = registeredTools.project_map_init; const result = await tool.execute("tool-1", {}, null, null, mockCtx); expect(result.details.success).toBe(false); expect(result.content[0].text).toContain("Pi LLM not accessible"); }); }); describe("project_map_patch tool", () => { it("returns error when Pi LLM is not accessible", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); const file = join(dir, "test.ts"); writeFileSync(file, `export const x = ${Date.now()};`); // Create a .pi-map.md so patchFile doesn't return early writeFileSync( join(dir, ".pi-map.md"), "# .\n## role\nTest\n## files\n## arch\n## dirty\n-\n", ); mockCtx.cwd = dir; const tool = registeredTools.project_map_patch; const result = await tool.execute( "tool-1", { file_path: file }, null, null, mockCtx, ); expect(result.details.success).toBe(false); expect(result.content[0].text).toContain("Pi LLM not accessible"); }); }); describe("project_map_validate tool", () => { it("reports clean when map exists and is up to date", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); writeFileSync( join(dir, ".pi-map.md"), "# .\n## role\nTest\n## files\n## arch\n## dirty\n-\n", ); mockCtx.cwd = dir; const tool = registeredTools.project_map_validate; const result = await tool.execute("tool-1", {}, null, null, mockCtx); expect(result.details.clean).toBe(true); expect(result.content[0].text).toContain("clean"); }); it("reports missing when files exist but no map", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); writeFileSync(join(dir, "test.ts"), "export const x = 1;"); mockCtx.cwd = dir; const tool = registeredTools.project_map_validate; const result = await tool.execute("tool-1", {}, null, null, mockCtx); expect(result.details.clean).toBe(false); expect(result.content[0].text).toContain("missing"); }); }); describe("project_map_reinit tool", () => { it("returns error when Pi LLM is not accessible", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); writeFileSync(join(dir, "test.ts"), `export const x = ${Date.now()};`); mockCtx.cwd = dir; const tool = registeredTools.project_map_reinit; const result = await tool.execute("tool-1", {}, null, null, mockCtx); expect(result.details.success).toBe(false); expect(result.content[0].text).toContain("Pi LLM not accessible"); }); }); describe("session_start event", () => { it("notifies when dirty .pi-map.md files exist", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); writeFileSync( join(dir, ".pi-map.md"), "# .\n## dirty\n2024-01-01: patched\n", ); mockCtx.cwd = dir; const handler = registeredEvents.session_start; await handler(null, mockCtx); expect(mockNotify).toHaveBeenCalledWith( expect.stringContaining("1 dirty packages detected"), "warning", ); }); it("does not notify when all clean", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); writeFileSync(join(dir, ".pi-map.md"), "# .\n## dirty\n-\n"); mockCtx.cwd = dir; const handler = registeredEvents.session_start; await handler(null, mockCtx); expect(mockNotify).not.toHaveBeenCalled(); }); it("does nothing when no maps exist", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); mockCtx.cwd = dir; const handler = registeredEvents.session_start; await handler(null, mockCtx); expect(mockNotify).not.toHaveBeenCalled(); }); }); describe("before_agent_start event", () => { it("injects hint when .pi-map.md files exist", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n"); mockCtx.cwd = dir; const handler = registeredEvents.before_agent_start; const result = await handler(null, mockCtx); expect(result).toHaveProperty("message"); expect(result.message.content).toContain("project_map_patch"); expect(result.message.display).toBe(false); }); it("returns empty object when no maps exist", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-")); mockCtx.cwd = dir; const handler = registeredEvents.before_agent_start; const result = await handler(null, mockCtx); expect(result).toEqual({}); }); }); });