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%
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