/** * KHAELOR * File: tests/tools/glob.test.ts * Description: Unit tests for the glob tool — pattern matching, mtime ordering, ignore behavior, caps. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { readFileSync, rmSync, utimesSync } from "node:fs"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createGlobTool } from "../../src/tools/index.js"; import type { TestHarness } from "./helpers.js"; import { fixture, makeHarness } from "./helpers.js"; let h: TestHarness; const glob = createGlobTool(); beforeEach(() => { h = makeHarness(); }); afterEach(() => { rmSync(h.dir, { recursive: true, force: true }); }); describe("glob tool", () => { it("matches nested files and orders newest first", async () => { const oldFile = fixture(h, "src/old.ts", "x\n"); const newFile = fixture(h, "src/deep/new.ts", "x\n"); fixture(h, "src/skip.py", "x\n"); const now = Date.now() / 1000; utimesSync(oldFile, now - 1000, now - 1000); utimesSync(newFile, now, now); const result = await glob.execute({ pattern: "**/*.ts" }, h.ctx); expect(result.isError).toBeUndefined(); const lines = result.content.split("\n"); expect(lines[0]).toBe('2 files match "**/*.ts" (newest first):'); expect(lines[1]).toBe("src/deep/new.ts"); expect(lines[2]).toBe("src/old.ts"); expect(result.metadata?.files).toBe(2); }); it("treats a slash-less pattern as a basename match at any depth", async () => { fixture(h, "a/b/config.json", "{}\n"); const result = await glob.execute({ pattern: "config.*" }, h.ctx); expect(result.content).toContain("a/b/config.json"); }); it("ignores node_modules, .git-style noise, and .gitignore entries", async () => { fixture(h, ".gitignore", "coverage/\n"); fixture(h, "node_modules/pkg/index.ts", "x\n"); fixture(h, "dist/out.ts", "x\n"); fixture(h, "coverage/cov.ts", "x\n"); fixture(h, "src/keep.ts", "x\n"); const result = await glob.execute({ pattern: "**/*.ts" }, h.ctx); expect(result.content).toContain("src/keep.ts"); expect(result.content).not.toContain("node_modules"); expect(result.content).not.toContain("dist/out.ts"); expect(result.content).not.toContain("coverage/cov.ts"); expect(result.metadata?.files).toBe(1); }); it("searches built-in ignored dirs when the pattern targets them explicitly", async () => { fixture(h, "dist/out.ts", "x\n"); const result = await glob.execute({ pattern: "dist/**/*.ts" }, h.ctx); expect(result.content).toContain("dist/out.ts"); }); it("reports zero matches with a nearest-extension hint", async () => { fixture(h, "src/a.ts", "x\n"); fixture(h, "src/b.ts", "x\n"); const result = await glob.execute({ pattern: "src/**/*.tsx" }, h.ctx); expect(result.isError).toBeUndefined(); expect(result.content).toContain(`No files match "src/**/*.tsx" under ${h.dir}.`); expect(result.content).toContain("Nearest existing extension: .ts (2 files)."); }); it("caps at 100 paths and spills the full list", async () => { for (let i = 0; i < 120; i++) fixture(h, `many/f${String(i).padStart(3, "0")}.ts`, "x\n"); const result = await glob.execute({ pattern: "many/*.ts" }, h.ctx); expect(result.content).toContain('120 files match "many/*.ts" (newest first):'); expect(result.content).toContain("[Showing 100 of 120. Full list: "); expect(result.content).toContain("or narrow the pattern.]"); const spill = result.metadata?.truncation?.spillPath; expect(spill).toBeDefined(); expect(readFileSync(spill!, "utf8").split("\n")).toHaveLength(120); }); it("scopes the search with the path parameter", async () => { fixture(h, "src/a.ts", "x\n"); fixture(h, "other/b.ts", "x\n"); const result = await glob.execute({ pattern: "*.ts", path: "src" }, h.ctx); expect(result.content).toContain("a.ts"); expect(result.content).not.toContain("b.ts"); }); });