import { describe, expect, mock, test } from "bun:test"; const encoder = new TextEncoder(); const blobs: Record = { a: "hello world\nnothing\nHello again", b: "hello from a dependency", c: new Uint8Array([104, 101, 108, 108, 111, 0, 1]), d: "hello, edited in the room", }; const entries = [ { path: "src/a.ts", sha: "a", type: "blob" }, { path: "node_modules/x/b.js", sha: "b", type: "blob" }, { path: "logo.png", sha: "c", type: "blob" }, { path: "src/d.ts", sha: "d", type: "exec" }, { path: "lib", sha: "s", type: "symlink" }, ]; mock.module("cloudflare:workers", () => ({ env: {}, waitUntil: () => undefined })); const actualSession = await import("./session"); mock.module("./session", () => ({ ...actualSession, editorSessionFromRequest: async () => ({ repo: { id: "r1", artifacts_name: "art", owner_name: "o", name: "n" }, revision: { name: "main", kind: "branch", sha: "c0" }, viewer: { id: "u1", name: "alice" }, }), canWrite: () => true, needsPullRequest: async () => false, })); const actualLimits = await import("../limits"); mock.module("../limits", () => ({ ...actualLimits, limitBrowse: async () => undefined, limitWrite: async () => undefined, })); const actualStore = await import("../git/store"); mock.module("../git/store", () => ({ ...actualStore, withStore: async (_name: string, run: (store: unknown) => Promise) => run({ commit: async () => ({ treeSha: "t0" }), walk: async () => ({ entries, truncated: false }), blob: async (sha: string) => { const value = blobs[sha]; return typeof value === "string" ? encoder.encode(value) : (value ?? null); }, }), })); let indexDown = false; const grepCalls: Array> = []; const { compileQuery, matchLines } = await import("~/lib/text-search"); mock.module("../code/repo-search-do", () => ({ repoSearch: () => ({ // The index holds src/a.ts at its head blob, and a stale copy of src/d.ts. grep: async (_repoId: string, query: never, head: Array<[string, string]>, limit: number) => { grepCalls.push(head); if (indexDown) throw new Error("index unavailable"); const index = new Map([ ["src/a.ts", { sha: "a", content: "hello world\nnothing\nHello again" }], ["src/d.ts", { sha: "d-old", content: "hello, stale" }], ]); const covered = head.filter(([path, sha]) => index.get(path)?.sha === sha); return { files: covered.map(([path]) => ({ path, matches: matchLines(index.get(path)?.content ?? "", compileQuery(query), limit), })), uncovered: head.filter((entry) => !covered.includes(entry)).map(([path]) => path), limitHit: false, }; }, }), })); const { handleEditorRoutes } = await import("./api"); async function search(body: unknown) { const url = new URL("https://forge.test/api/editor/search"); const response = await handleEditorRoutes( new Request(url, { method: "POST", headers: { authorization: "Bearer t" }, body: JSON.stringify(body) }), url, ); if (!response) throw new Error("no route"); return { status: response.status, body: (await response.json()) as { files: { path: string }[]; limitHit: boolean } }; } describe("editor workspace search", () => { test("greps text files at the head, skipping excluded dirs, binaries, symlinks and room-held paths", async () => { const result = await search({ pattern: "hello", excludeDirs: ["node_modules"], skip: ["src/d.ts"] }); expect(result.status).toBe(200); expect(result.body.files).toEqual([ { path: "src/a.ts", matches: [ { line: 0, text: "hello world", ranges: [[0, 5]] }, { line: 2, text: "Hello again", ranges: [[0, 5]] }, ], }, ] as never); expect(result.body.limitHit).toBe(false); }); test("indexed blobs are searched in the index and only the rest are read", async () => { grepCalls.length = 0; const result = await search({ pattern: "hello", excludeDirs: ["node_modules"] }); expect(grepCalls[0]?.map(([path]) => path)).toEqual(["src/a.ts", "logo.png", "src/d.ts"]); expect(result.body.files.map((file) => file.path)).toEqual(["src/a.ts", "src/d.ts"]); expect(JSON.stringify(result.body)).not.toContain("stale"); }); test("an unavailable index falls back to reading every blob", async () => { indexDown = true; try { const result = await search({ pattern: "hello", excludeDirs: ["node_modules"] }); expect(result.body.files.map((file) => file.path)).toEqual(["src/a.ts", "src/d.ts"]); } finally { indexDown = false; } }); test("stops at maxResults and reports the limit", async () => { const result = await search({ pattern: "hello", maxResults: 1 }); expect(result.body.files).toHaveLength(1); expect(result.body.limitHit).toBe(true); }); test("rejects an invalid regular expression", async () => { expect((await search({ pattern: "(", isRegExp: true })).status).toBe(400); }); });