/** * KHAELOR * File: tests/repository/search.test.ts * Description: FileSearch tests — fuzzy ranking tiers (exact > prefix > subsequence) and frecency boosts. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { rmSync } from "node:fs"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { FileSearch, RecentFilesTracker, RepositoryMap, subsequenceQuality, } from "../../src/repository/index.js"; import { LocalWorkspace } from "../../src/workspace/index.js"; import { makeTempDir, writeFixtureFile } from "./fixtures.js"; let dir: string; let map: RepositoryMap; let recent: RecentFilesTracker; let search: FileSearch; beforeEach(() => { dir = makeTempDir("khaelor-search-"); for (const rel of [ "abc.ts", "abcdef.ts", "axbxcx.ts", "src/alpha/agent.ts", "src/betaa/agent.ts", "notes.md", ]) { writeFixtureFile(dir, rel, "content\n"); } map = new RepositoryMap(new LocalWorkspace(dir)); recent = new RecentFilesTracker(); search = new FileSearch(map, recent); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); describe("FileSearch ranking tiers", () => { it("ranks exact matches above prefix matches above subsequence matches", async () => { const exact = await search.findFiles("abc.ts"); expect(exact[0]?.path).toBe("abc.ts"); const prefix = await search.findFiles("abc"); expect(prefix.map((r) => r.path)).toEqual(["abc.ts", "abcdef.ts", "axbxcx.ts"]); expect(prefix[0]?.score).toBeGreaterThan(prefix[2]?.score ?? 0); }); it("excludes files that are not even a subsequence match", async () => { expect(await search.findFiles("zzz")).toEqual([]); expect(subsequenceQuality("notes.md", "zzz")).toBeUndefined(); expect(subsequenceQuality("axbxcx.ts", "abc")).toBeGreaterThan(0); }); it("respects the result limit", async () => { const results = await search.findFiles("a", 2); expect(results).toHaveLength(2); }); }); describe("FileSearch frecency boost", () => { it("breaks ties deterministically without access history", async () => { const results = await search.findFiles("agent"); expect(results.map((r) => r.path)).toEqual(["src/alpha/agent.ts", "src/betaa/agent.ts"]); }); it("boosts recently accessed files within the same match tier", async () => { recent.noteAccess("src/betaa/agent.ts"); const results = await search.findFiles("agent"); expect(results.map((r) => r.path)).toEqual(["src/betaa/agent.ts", "src/alpha/agent.ts"]); }); it("never lets frecency promote a weaker match class above a stronger one", async () => { // axbxcx.ts is only a subsequence match for "abc" — heavy access must not // rank it above the prefix matches. for (let i = 0; i < 10; i += 1) recent.noteAccess("axbxcx.ts"); const results = await search.findFiles("abc"); expect(results.map((r) => r.path)).toEqual(["abc.ts", "abcdef.ts", "axbxcx.ts"]); }); }); describe("FileSearch empty query", () => { it("returns recently accessed files first, then alphabetical fill", async () => { recent.noteAccess("notes.md"); const results = await search.findFiles("", 4); expect(results[0]?.path).toBe("notes.md"); expect(results).toHaveLength(4); const rest = results.slice(1).map((r) => r.path); expect(rest).toEqual([...rest].sort((a, b) => a.localeCompare(b))); }); });