import fsPromises from "node:fs/promises"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it, afterEach, describe, expect, vi } from "@effect/vitest"; import { Effect, FileSystem, Layer, Path, PlatformError } from "effect"; import { ServerConfig } from "../../config.ts"; import { GitCoreLive } from "../../git/Layers/GitCore.ts"; import { GitCore } from "../../git/Services/GitCore.ts"; import { WorkspaceEntries } from "../Services/WorkspaceEntries.ts"; import { WorkspaceEntriesLive } from "./WorkspaceEntries.ts"; import { WorkspacePathsLive } from "./WorkspacePaths.ts"; const TestLayer = Layer.empty.pipe( Layer.provideMerge(WorkspaceEntriesLive.pipe(Layer.provide(WorkspacePathsLive))), Layer.provideMerge(WorkspacePathsLive), Layer.provideMerge(GitCoreLive), Layer.provide( ServerConfig.layerTest(process.cwd(), { prefix: "t3-workspace-entries-test-", }), ), Layer.provideMerge(NodeServices.layer), ); const makeTempDir = Effect.fn(function* (opts?: { prefix?: string; git?: boolean }) { const fileSystem = yield* FileSystem.FileSystem; const gitCore = yield* GitCore; const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: opts?.prefix ?? "t3code-workspace-entries-", }); if (opts?.git) { yield* gitCore.initRepo({ cwd: dir }); } return dir; }); function writeTextFile( cwd: string, relativePath: string, contents = "", ): Effect.Effect { return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const absolutePath = path.join(cwd, relativePath); yield* fileSystem.makeDirectory(path.dirname(absolutePath), { recursive: true }); yield* fileSystem.writeFileString(absolutePath, contents); }); } const git = (cwd: string, args: ReadonlyArray, env?: NodeJS.ProcessEnv) => Effect.gen(function* () { const gitCore = yield* GitCore; const result = yield* gitCore.execute({ operation: "WorkspaceEntries.test.git", cwd, args, ...(env ? { env } : {}), timeoutMs: 10_000, }); return result.stdout.trim(); }); const searchWorkspaceEntries = (input: { cwd: string; query: string; limit: number }) => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries; return yield* workspaceEntries.search(input); }); const appendSeparator = (input: string) => input.endsWith("/") || input.endsWith("\\") ? input : `${input}${process.platform === "win32" ? "\\" : "/"}`; it.layer(TestLayer)("WorkspaceEntriesLive", (it) => { afterEach(() => { vi.restoreAllMocks(); }); describe("search", () => { it.effect("returns files and directories relative to cwd", () => Effect.gen(function* () { const cwd = yield* makeTempDir(); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/index.ts"); yield* writeTextFile(cwd, "README.md"); yield* writeTextFile(cwd, ".git/HEAD"); yield* writeTextFile(cwd, "node_modules/pkg/index.js"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/components"); expect(paths).toContain("src/components/Composer.tsx"); expect(paths).toContain("README.md"); expect(paths.some((entryPath) => entryPath.startsWith(".git"))).toBe(false); expect(paths.some((entryPath) => entryPath.startsWith("node_modules"))).toBe(false); expect(result.truncated).toBe(false); }), ); it.effect("filters and ranks entries by query", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-query-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "compo", limit: 5 }); expect(result.entries.length).toBeGreaterThan(0); expect(result.entries.some((entry) => entry.path === "src/components")).toBe(true); expect(result.entries.every((entry) => entry.path.toLowerCase().includes("compo"))).toBe( true, ); }), ); it.effect("supports fuzzy subsequence queries for composer path search", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fuzzy-query-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "cmp", limit: 10 }); const paths = result.entries.map((entry) => entry.path); expect(result.entries.length).toBeGreaterThan(0); expect(paths).toContain("src/components"); expect(paths).toContain("src/components/Composer.tsx"); }), ); it.effect("prioritizes exact basename matches ahead of broader path matches", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-exact-ranking-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "docs/composer.tsx-notes.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "Composer.tsx", limit: 5 }); expect(result.entries[0]?.path).toBe("src/components/Composer.tsx"); }), ); it.effect("tracks truncation without sorting every fuzzy match", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fuzzy-limit-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "cmp", limit: 1 }); expect(result.entries).toHaveLength(1); expect(result.truncated).toBe(true); }), ); it.effect("excludes gitignored paths for git repositories", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-gitignore-", git: true }); yield* writeTextFile(cwd, ".gitignore", ".convex/\nconvex/\nignored.txt\n"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); yield* writeTextFile(cwd, "ignored.txt", "ignore me"); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "convex/UOoS-l/convex_local_storage/modules/data.json", "{}"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths).not.toContain("ignored.txt"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); expect(paths.some((entryPath) => entryPath.startsWith("convex/"))).toBe(false); }), ); it.effect("excludes tracked paths that match ignore rules", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-tracked-gitignore-", git: true, }); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); yield* git(cwd, ["add", ".convex/local-storage/data.json", "src/keep.ts"]); yield* writeTextFile(cwd, ".gitignore", ".convex/\n"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); }), ); it.effect("excludes .convex in non-git workspaces", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-non-git-convex-" }); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); }), ); it.effect("deduplicates concurrent index builds for the same cwd", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-concurrent-build-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); let rootReadCount = 0; const originalReaddir = fsPromises.readdir.bind(fsPromises); vi.spyOn(fsPromises, "readdir").mockImplementation((async ( ...args: Parameters ) => { if (args[0] === cwd) { rootReadCount += 1; await new Promise((resolve) => setTimeout(resolve, 20)); } return originalReaddir(...args); }) as typeof fsPromises.readdir); yield* Effect.all( [ searchWorkspaceEntries({ cwd, query: "", limit: 100 }), searchWorkspaceEntries({ cwd, query: "comp", limit: 100 }), searchWorkspaceEntries({ cwd, query: "src", limit: 100 }), ], { concurrency: "unbounded" }, ); expect(rootReadCount).toBe(1); }), ); it.effect("limits concurrent directory reads while walking the filesystem", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-read-concurrency-" }); yield* Effect.forEach( Array.from({ length: 80 }, (_, index) => index), (index) => writeTextFile(cwd, `group-${index}/entry-${index}.ts`, "export {};"), { discard: true }, ); let activeReads = 0; let peakReads = 0; const originalReaddir = fsPromises.readdir.bind(fsPromises); vi.spyOn(fsPromises, "readdir").mockImplementation((async ( ...args: Parameters ) => { const target = args[0]; if (typeof target === "string" && target.startsWith(cwd)) { activeReads += 1; peakReads = Math.max(peakReads, activeReads); await new Promise((resolve) => setTimeout(resolve, 4)); try { return await originalReaddir(...args); } finally { activeReads -= 1; } } return originalReaddir(...args); }) as typeof fsPromises.readdir); yield* searchWorkspaceEntries({ cwd, query: "", limit: 200 }); expect(peakReads).toBeLessThanOrEqual(32); }), ); }); describe("browse", () => { it.effect("returns matching directories and excludes files", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries; const path = yield* Path.Path; const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-prefix-" }); yield* writeTextFile(cwd, "alphabet.txt", "ignore me"); yield* writeTextFile(cwd, "alpha/index.ts", "export {};\n"); yield* writeTextFile(cwd, "alpine/index.ts", "export {};\n"); const result = yield* workspaceEntries.browse({ partialPath: path.join(cwd, "alp"), }); expect(result).toEqual({ parentPath: cwd, entries: [ { name: "alpha", fullPath: path.join(cwd, "alpha") }, { name: "alpine", fullPath: path.join(cwd, "alpine") }, ], }); }), ); it.effect("shows dot directories in directory mode and hidden-prefix mode", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries; const path = yield* Path.Path; const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-hidden-" }); yield* writeTextFile(cwd, ".config/settings.json", "{}"); yield* writeTextFile(cwd, "config/settings.json", "{}"); const directoryResult = yield* workspaceEntries.browse({ partialPath: appendSeparator(cwd), }); const hiddenPrefixResult = yield* workspaceEntries.browse({ partialPath: `${appendSeparator(cwd)}.c`, }); expect(directoryResult.entries.map((entry) => entry.name)).toEqual([".config", "config"]); expect(hiddenPrefixResult).toEqual({ parentPath: cwd, entries: [{ name: ".config", fullPath: path.join(cwd, ".config") }], }); }), ); it.effect("supports relative paths when cwd is provided", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries; const path = yield* Path.Path; const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-relative-" }); yield* writeTextFile(cwd, "packages/pkg.json", "{}"); const result = yield* workspaceEntries.browse({ cwd, partialPath: "./pack", }); expect(result).toEqual({ parentPath: cwd, entries: [{ name: "packages", fullPath: path.join(cwd, "packages") }], }); }), ); it.effect("rejects relative paths without cwd", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries; const error = yield* workspaceEntries .browse({ partialPath: "./src", }) .pipe(Effect.flip); expect(error.detail).toBe("Relative filesystem browse paths require a current project."); }), ); }); });