SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
6.4 KB · 166 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/repository/map.test.ts4 * Description: RepositoryMap tests — bounds, gitignore pruning, rendering caps, incremental refresh.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { rmSync, utimesSync } from "node:fs";11import * as path from "node:path";12import { afterEach, beforeEach, describe, expect, it } from "vitest";13import { RepositoryMap, languageOf, prunableNamesFromIgnore } from "../../src/repository/index.js";14import { LocalWorkspace } from "../../src/workspace/index.js";15import { makeTempDir, writeFixtureFile } from "./fixtures.js";1617let dir: string;18let ws: LocalWorkspace;1920beforeEach(() => {21  dir = makeTempDir("khaelor-map-");22  ws = new LocalWorkspace(dir);23  writeFixtureFile(dir, ".gitignore", "# build output\ndist/\n*.log\n!keep.log\n");24  writeFixtureFile(dir, "src/a.ts", "export const a = 1;\n");25  writeFixtureFile(dir, "src/sub/b.ts", "export const b = 2;\n");26  writeFixtureFile(dir, "README.md", "# fixture\n");27  writeFixtureFile(dir, "node_modules/pkg/index.js", "module.exports = {};\n");28  writeFixtureFile(dir, "dist/out.js", "built\n");29  writeFixtureFile(dir, ".git/config", "[core]\n");30});3132afterEach(() => {33  rmSync(dir, { recursive: true, force: true });34});3536/** Force a directory mtime forward so refresh sees it as changed (second-granularity stats). */37function bumpDir(rel: string): void {38  const future = new Date(Date.now() + 5000);39  utimesSync(path.join(dir, rel), future, future);40}4142describe("RepositoryMap build", () => {43  it("maps files with metadata and prunes .git, node_modules, and gitignored dirs", async () => {44    const map = new RepositoryMap(ws);45    const entries = await map.fileMap();46    const paths = entries.map((e) => e.path);4748    expect(paths).toContain("src/a.ts");49    expect(paths).toContain("src/sub/b.ts");50    expect(paths).toContain("README.md");51    expect(paths).toContain("src");52    expect(paths.some((p) => p.startsWith("node_modules"))).toBe(false);53    expect(paths.some((p) => p === ".git" || p.startsWith(".git/"))).toBe(false);54    expect(paths).toContain(".gitignore"); // the ignore FILE is mapped; the .git DIR is pruned55    expect(paths.some((p) => p.startsWith("dist"))).toBe(false);5657    const a = entries.find((e) => e.path === "src/a.ts");58    expect(a).toMatchObject({ kind: "file", language: "typescript" });59    expect(a !== undefined && a.size > 0).toBe(true);60    expect(a !== undefined && a.mtimeMs > 0).toBe(true);61  });6263  it("respects the depth cap", async () => {64    writeFixtureFile(dir, "deep/d1/d2/d3/deep.ts", "too deep\n");65    const map = new RepositoryMap(ws, { maxDepth: 3 });66    const paths = (await map.fileMap()).map((e) => e.path);67    expect(paths).toContain("src/sub/b.ts"); // depth 3 — included68    expect(paths).not.toContain("deep/d1/d2/d3/deep.ts"); // depth 5 — excluded69  });7071  it("caps fileMap exposure at maxEntries and reports truncation", async () => {72    const map = new RepositoryMap(ws);73    const entries = await map.fileMap({ maxEntries: 3 });74    expect(entries).toHaveLength(3);75    expect(map.wasTruncated()).toBe(true);76    await map.fileMap();77    expect(map.wasTruncated()).toBe(false);78  });7980  it("parses only simple prunable names from ignore files", () => {81    const names = prunableNamesFromIgnore("# c\n\ndist/\n/build\n!keep\nfoo/bar\n*.log\ntmp\n");82    expect(names).toEqual(["dist", "build", "tmp"]);83  });8485  it("infers languages from extensions", () => {86    expect(languageOf("src/kernel/agent.ts")).toBe("typescript");87    expect(languageOf("scripts/build.sh")).toBe("shell");88    expect(languageOf("README.md")).toBe("markdown");89    expect(languageOf("Makefile")).toBeUndefined();90    expect(languageOf(".gitignore")).toBeUndefined();91  });92});9394describe("RepositoryMap rendering", () => {95  it("renders a compact indented tree without pruned directories", async () => {96    const map = new RepositoryMap(ws);97    const rendered = await map.render();98    expect(rendered).toContain("src/");99    expect(rendered).toContain("a.ts");100    expect(rendered).toContain("README.md");101    expect(rendered).not.toContain("node_modules");102    expect(rendered).not.toContain("dist");103  });104105  it("hard-caps rendered output at the byte budget with an omission marker", async () => {106    for (let i = 0; i < 40; i += 1) {107      writeFixtureFile(dir, `many/file-${String(i).padStart(2, "0")}.ts`, "x\n");108    }109    const map = new RepositoryMap(ws);110    const rendered = await map.render(120);111    expect(Buffer.byteLength(rendered, "utf8")).toBeLessThanOrEqual(120);112    expect(rendered).toContain("more entries not shown");113  });114});115116describe("RepositoryMap incremental refresh", () => {117  it("picks up new files in existing directories", async () => {118    const map = new RepositoryMap(ws);119    await map.build();120    expect((await map.files()).map((e) => e.path)).not.toContain("src/added.ts");121122    writeFixtureFile(dir, "src/added.ts", "export const added = true;\n");123    bumpDir("src");124    await map.refresh();125    expect((await map.files()).map((e) => e.path)).toContain("src/added.ts");126  });127128  it("picks up new directories with their files", async () => {129    const map = new RepositoryMap(ws);130    await map.build();131    writeFixtureFile(dir, "newdir/n.ts", "export const n = 1;\n");132    bumpDir("newdir");133    await map.refresh();134    const paths = (await map.fileMap()).map((e) => e.path);135    expect(paths).toContain("newdir");136    expect(paths).toContain("newdir/n.ts");137  });138139  it("drops deleted files and directories", async () => {140    const map = new RepositoryMap(ws);141    await map.build();142    rmSync(path.join(dir, "src/a.ts"));143    rmSync(path.join(dir, "src/sub"), { recursive: true });144    bumpDir("src");145    await map.refresh();146    const paths = (await map.fileMap()).map((e) => e.path);147    expect(paths).not.toContain("src/a.ts");148    expect(paths).not.toContain("src/sub");149    expect(paths).not.toContain("src/sub/b.ts");150  });151});152153describe.runIf(process.platform === "darwin")("RepositoryMap forced BSD stat flavor", () => {154  it("builds and refreshes identically through the stat scanner", async () => {155    const map = new RepositoryMap(ws, { flavor: "stat" });156    const paths = (await map.fileMap()).map((e) => e.path);157    expect(paths).toContain("src/a.ts");158    expect(paths.some((p) => p.startsWith("node_modules"))).toBe(false);159160    writeFixtureFile(dir, "src/added.ts", "x\n");161    bumpDir("src");162    await map.refresh();163    expect((await map.files()).map((e) => e.path)).toContain("src/added.ts");164  });165});166