import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtempSync, writeFileSync, mkdirSync, rmSync, readFileSync, } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { initProject } from "../src/init.js"; import { patchFile } from "../src/patch.js"; import { validateMaps } from "../src/validate.js"; import { createMockFileClient } from "./mock-llm.js"; describe("integration", () => { let dir: string; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "pi-map-int-")); }); afterEach(() => { rmSync(dir, { recursive: true }); }); it("init creates both .pi-map.md and .pi-map.index.md files", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8"); const index = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"); expect(map).toContain("# src"); expect(index).toContain("# src (index)"); expect(index).toContain("dir: src"); }); it("root artifacts contain Project Map Protocol", async () => { writeFileSync(join(dir, "package.json"), `{}\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8"); const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8"); expect(rootMap).toContain("## Project Map Protocol"); expect(rootMap).toContain("index: ./.pi-map.index.md"); expect(rootIndex).toContain("## Project Map Protocol"); expect(rootIndex).toContain("map: ./.pi-map.md"); }); it("non-root map contains index link", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8"); expect(map).toContain("index: src/.pi-map.index.md"); expect(map).toContain("dir: src"); }); it("index contains parent and children when applicable", async () => { mkdirSync(join(dir, "src")); mkdirSync(join(dir, "src", "utils")); writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`); writeFileSync( join(dir, "src", "utils", "helpers.ts"), `export const h = 1;\n`, ); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const srcIndex = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"); expect(srcIndex).toContain("## parent"); expect(srcIndex).toContain("index: ./.pi-map.index.md"); expect(srcIndex).toContain("## children"); expect(srcIndex).toContain("- src/utils"); const utilsIndex = readFileSync( join(dir, "src", "utils", ".pi-map.index.md"), "utf8", ); expect(utilsIndex).toContain("## parent"); expect(utilsIndex).toContain("index: src/.pi-map.index.md"); }); it("patch updates a file entry", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`); writeFileSync(join(dir, "src", "utils.ts"), `export const bar = 1;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); // Modify a file writeFileSync( join(dir, "src", "index.ts"), `export function foo() {}\nexport function baz() {}\n`, ); await patchFile(join(dir, "src", "index.ts"), client, dir); const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8"); expect(map).toContain("baz"); }); it("patch with small mode refreshes ancestor indexes but preserves ancestor maps", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "package.json"), `{}\n`); writeFileSync(join(dir, "src", "index.ts"), `export const a = 1;\n`); writeFileSync(join(dir, "src", "helper.ts"), `export const b = 2;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); writeFileSync( join(dir, ".pi-map.md"), `${readFileSync(join(dir, ".pi-map.md"), "utf8")}\nSENTINEL_ROOT_MAP\n`, ); writeFileSync( join(dir, ".pi-map.index.md"), `${readFileSync(join(dir, ".pi-map.index.md"), "utf8")}\nSENTINEL_ROOT_INDEX\n`, ); writeFileSync( join(dir, "src", "index.ts"), `export const a = 1;\nexport const c = 3;\n`, ); await patchFile(join(dir, "src", "index.ts"), client, dir, { rootPath: dir, patchMode: "small", }); const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8"); const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8"); expect(rootMap).toContain("SENTINEL_ROOT_MAP"); expect(rootIndex).not.toContain("SENTINEL_ROOT_INDEX"); }); it("patch with structural mode refreshes ancestor maps and indexes", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "package.json"), `{}\n`); writeFileSync(join(dir, "src", "index.ts"), `export const a = 1;\n`); writeFileSync(join(dir, "src", "helper.ts"), `export const b = 2;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); writeFileSync( join(dir, ".pi-map.md"), `${readFileSync(join(dir, ".pi-map.md"), "utf8")}\nSENTINEL_ROOT_MAP\n`, ); writeFileSync( join(dir, ".pi-map.index.md"), `${readFileSync(join(dir, ".pi-map.index.md"), "utf8")}\nSENTINEL_ROOT_INDEX\n`, ); writeFileSync( join(dir, "src", "index.ts"), `export const a = 1;\nexport const c = 3;\n`, ); await patchFile(join(dir, "src", "index.ts"), client, dir, { rootPath: dir, patchMode: "structural", }); const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8"); const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8"); expect(rootMap).not.toContain("SENTINEL_ROOT_MAP"); expect(rootIndex).not.toContain("SENTINEL_ROOT_INDEX"); }); it("validate detects new files", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); // Add new file writeFileSync(join(dir, "src", "b.ts"), `export const b = 2;\n`); const result = await validateMaps(dir); expect(result.clean).toBe(false); expect(result.discrepancies.some((d) => d.type === "missing")).toBe(true); }); it("validate detects deleted files", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`); writeFileSync(join(dir, "src", "b.ts"), `export const b = 2;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); // Delete a file rmSync(join(dir, "src", "b.ts")); const result = await validateMaps(dir); expect(result.clean).toBe(false); expect(result.discrepancies.some((d) => d.type === "orphaned")).toBe(true); }); it("validate detects changed signatures", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); // Change exports writeFileSync( join(dir, "src", "a.ts"), `export const a = 1;\nexport const c = 3;\n`, ); const result = await validateMaps(dir); expect(result.clean).toBe(false); expect(result.discrepancies.some((d) => d.type === "stale-signature")).toBe( true, ); }); it("init populates routing metadata in rich maps", async () => { mkdirSync(join(dir, "src")); writeFileSync( join(dir, "src", "index.ts"), `export function init() {}\nexport function run() {}\n`, ); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8"); // Tags and symbols should be scaffolded or populated expect(map).toContain("## tags"); expect(map).toContain("## symbols"); expect(map).toContain("## workflows"); }); it("leaf directory index is tiny but present", async () => { mkdirSync(join(dir, "src")); mkdirSync(join(dir, "src", "utils")); writeFileSync( join(dir, "src", "utils", "helpers.ts"), `export const h = 1;\n`, ); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const index = readFileSync( join(dir, "src", "utils", ".pi-map.index.md"), "utf8", ); // Leaf index should have empty children but still follow contract expect(index).toContain("## children"); expect(index).toContain("-"); expect(index).toContain("## parent"); expect(index).toContain("index: src/.pi-map.index.md"); }); it("non-source directories omit uncertain workflows", async () => { mkdirSync(join(dir, "docs")); writeFileSync(join(dir, "docs", "README.md"), `# README\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const map = readFileSync(join(dir, "docs", ".pi-map.md"), "utf8"); // No source files means workflows section should be empty/omitted const workflowsMatch = map.match(/## workflows\n([^#]*)/); if (workflowsMatch) { expect(workflowsMatch[1].trim()).toBe("-"); } }); it("init loads config caps and applies them to routing metadata", async () => { mkdirSync(join(dir, "src")); for (let i = 0; i < 20; i++) { writeFileSync( join(dir, "src", `file${i}.ts`), `export function fn${i}() {}\n`, ); } writeFileSync( join(dir, ".pi-project-map.json"), JSON.stringify({ tagCap: 3, workflowHintCap: 2 }), ); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8"); const parsedMap = (await import("../src/format.js")).parseDirectoryMap(map); expect(parsedMap.tags.length).toBeLessThanOrEqual(3); expect(parsedMap.symbols.length).toBeLessThanOrEqual(3); const index = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"); const parsedIndex = (await import("../src/format.js")).parseDirectoryIndex( index, ); expect(parsedIndex.workflows.length).toBeLessThanOrEqual(2); }); it("validate hard-fails when an index is missing", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); rmSync(join(dir, "src", ".pi-map.index.md")); const result = await validateMaps(dir, { verbose: false }); expect(result.clean).toBe(false); expect(result.discrepancies.some((d) => d.type === "stale-index")).toBe( true, ); }); it("validate detects broken sibling links", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); const mapPath = join(dir, "src", ".pi-map.md"); writeFileSync( mapPath, readFileSync(mapPath, "utf8").replace( "index: src/.pi-map.index.md", "index: src/bad-index.md", ), ); const result = await validateMaps(dir, { verbose: false }); expect(result.clean).toBe(false); expect(result.discrepancies.some((d) => d.type === "broken-link")).toBe( true, ); }); it("validate --fix repairs the affected chain", async () => { mkdirSync(join(dir, "src")); writeFileSync(join(dir, "package.json"), `{}\n`); writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`); const client = createMockFileClient(); await initProject(dir, { llmClient: client, verbose: false }); rmSync(join(dir, "src", ".pi-map.index.md")); const result = await validateMaps(dir, { fix: true, verbose: false, llmClient: client, cacheDir: dir, patchMode: "small", }); expect(result.clean).toBe(false); expect(result.fixed).toBeGreaterThan(0); expect( readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"), ).toContain("# src (index)"); }); });