/** * KHAELOR * File: tests/repository/map.test.ts * Description: RepositoryMap tests — bounds, gitignore pruning, rendering caps, incremental refresh. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { rmSync, utimesSync } from "node:fs"; import * as path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { RepositoryMap, languageOf, prunableNamesFromIgnore } from "../../src/repository/index.js"; import { LocalWorkspace } from "../../src/workspace/index.js"; import { makeTempDir, writeFixtureFile } from "./fixtures.js"; let dir: string; let ws: LocalWorkspace; beforeEach(() => { dir = makeTempDir("khaelor-map-"); ws = new LocalWorkspace(dir); writeFixtureFile(dir, ".gitignore", "# build output\ndist/\n*.log\n!keep.log\n"); writeFixtureFile(dir, "src/a.ts", "export const a = 1;\n"); writeFixtureFile(dir, "src/sub/b.ts", "export const b = 2;\n"); writeFixtureFile(dir, "README.md", "# fixture\n"); writeFixtureFile(dir, "node_modules/pkg/index.js", "module.exports = {};\n"); writeFixtureFile(dir, "dist/out.js", "built\n"); writeFixtureFile(dir, ".git/config", "[core]\n"); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); /** Force a directory mtime forward so refresh sees it as changed (second-granularity stats). */ function bumpDir(rel: string): void { const future = new Date(Date.now() + 5000); utimesSync(path.join(dir, rel), future, future); } describe("RepositoryMap build", () => { it("maps files with metadata and prunes .git, node_modules, and gitignored dirs", async () => { const map = new RepositoryMap(ws); const entries = await map.fileMap(); const paths = entries.map((e) => e.path); expect(paths).toContain("src/a.ts"); expect(paths).toContain("src/sub/b.ts"); expect(paths).toContain("README.md"); expect(paths).toContain("src"); expect(paths.some((p) => p.startsWith("node_modules"))).toBe(false); expect(paths.some((p) => p === ".git" || p.startsWith(".git/"))).toBe(false); expect(paths).toContain(".gitignore"); // the ignore FILE is mapped; the .git DIR is pruned expect(paths.some((p) => p.startsWith("dist"))).toBe(false); const a = entries.find((e) => e.path === "src/a.ts"); expect(a).toMatchObject({ kind: "file", language: "typescript" }); expect(a !== undefined && a.size > 0).toBe(true); expect(a !== undefined && a.mtimeMs > 0).toBe(true); }); it("respects the depth cap", async () => { writeFixtureFile(dir, "deep/d1/d2/d3/deep.ts", "too deep\n"); const map = new RepositoryMap(ws, { maxDepth: 3 }); const paths = (await map.fileMap()).map((e) => e.path); expect(paths).toContain("src/sub/b.ts"); // depth 3 — included expect(paths).not.toContain("deep/d1/d2/d3/deep.ts"); // depth 5 — excluded }); it("caps fileMap exposure at maxEntries and reports truncation", async () => { const map = new RepositoryMap(ws); const entries = await map.fileMap({ maxEntries: 3 }); expect(entries).toHaveLength(3); expect(map.wasTruncated()).toBe(true); await map.fileMap(); expect(map.wasTruncated()).toBe(false); }); it("parses only simple prunable names from ignore files", () => { const names = prunableNamesFromIgnore("# c\n\ndist/\n/build\n!keep\nfoo/bar\n*.log\ntmp\n"); expect(names).toEqual(["dist", "build", "tmp"]); }); it("infers languages from extensions", () => { expect(languageOf("src/kernel/agent.ts")).toBe("typescript"); expect(languageOf("scripts/build.sh")).toBe("shell"); expect(languageOf("README.md")).toBe("markdown"); expect(languageOf("Makefile")).toBeUndefined(); expect(languageOf(".gitignore")).toBeUndefined(); }); }); describe("RepositoryMap rendering", () => { it("renders a compact indented tree without pruned directories", async () => { const map = new RepositoryMap(ws); const rendered = await map.render(); expect(rendered).toContain("src/"); expect(rendered).toContain("a.ts"); expect(rendered).toContain("README.md"); expect(rendered).not.toContain("node_modules"); expect(rendered).not.toContain("dist"); }); it("hard-caps rendered output at the byte budget with an omission marker", async () => { for (let i = 0; i < 40; i += 1) { writeFixtureFile(dir, `many/file-${String(i).padStart(2, "0")}.ts`, "x\n"); } const map = new RepositoryMap(ws); const rendered = await map.render(120); expect(Buffer.byteLength(rendered, "utf8")).toBeLessThanOrEqual(120); expect(rendered).toContain("more entries not shown"); }); }); describe("RepositoryMap incremental refresh", () => { it("picks up new files in existing directories", async () => { const map = new RepositoryMap(ws); await map.build(); expect((await map.files()).map((e) => e.path)).not.toContain("src/added.ts"); writeFixtureFile(dir, "src/added.ts", "export const added = true;\n"); bumpDir("src"); await map.refresh(); expect((await map.files()).map((e) => e.path)).toContain("src/added.ts"); }); it("picks up new directories with their files", async () => { const map = new RepositoryMap(ws); await map.build(); writeFixtureFile(dir, "newdir/n.ts", "export const n = 1;\n"); bumpDir("newdir"); await map.refresh(); const paths = (await map.fileMap()).map((e) => e.path); expect(paths).toContain("newdir"); expect(paths).toContain("newdir/n.ts"); }); it("drops deleted files and directories", async () => { const map = new RepositoryMap(ws); await map.build(); rmSync(path.join(dir, "src/a.ts")); rmSync(path.join(dir, "src/sub"), { recursive: true }); bumpDir("src"); await map.refresh(); const paths = (await map.fileMap()).map((e) => e.path); expect(paths).not.toContain("src/a.ts"); expect(paths).not.toContain("src/sub"); expect(paths).not.toContain("src/sub/b.ts"); }); }); describe.runIf(process.platform === "darwin")("RepositoryMap forced BSD stat flavor", () => { it("builds and refreshes identically through the stat scanner", async () => { const map = new RepositoryMap(ws, { flavor: "stat" }); const paths = (await map.fileMap()).map((e) => e.path); expect(paths).toContain("src/a.ts"); expect(paths.some((p) => p.startsWith("node_modules"))).toBe(false); writeFixtureFile(dir, "src/added.ts", "x\n"); bumpDir("src"); await map.refresh(); expect((await map.files()).map((e) => e.path)).toContain("src/added.ts"); }); });