// @effect-diagnostics nodeBuiltinImport:offimport * as NodeFSP from "node:fs/promises";import * as NodeServices from "@effect/platform-node/NodeServices";import { FileFinder } from "@ff-labs/fff-node";import { it, afterEach, describe, expect } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError";import { vi } from "vite-plus/test"; import * as ServerConfig from "../config.ts";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as VcsProcess from "../vcs/VcsProcess.ts";import * as WorkspaceEntries from "./WorkspaceEntries.ts";import * as WorkspacePaths from "./WorkspacePaths.ts"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal<typeof import("node:fs/promises")>(); return { ...actual, readdir: vi.fn(actual.readdir) };}); const TestLayer = Layer.empty.pipe( Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))), Layer.provideMerge(WorkspacePaths.layer), Layer.provideMerge(VcsProcess.layer), Layer.provide( ServerConfig.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 dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: opts?.prefix ?? "t3code-workspace-entries-", }); if (opts?.git) { yield* git(dir, ["init"]); } return dir;}); function writeTextFile( cwd: string, relativePath: string, contents = "",): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> { 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<string>, env?: NodeJS.ProcessEnv) => Effect.gen(function* () { const process = yield* VcsProcess.VcsProcess; const result = yield* process.run({ operation: "WorkspaceEntries.test.git", command: "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.WorkspaceEntries; return yield* workspaceEntries.search(input); }); const appendSeparator = (input: string) => Effect.map(HostProcessPlatform, (platform) => input.endsWith("/") || input.endsWith("\\") ? input : `${input}${platform === "win32" ? "\\" : "/"}`, ); it.layer(TestLayer, { excludeTestServices: true })("WorkspaceEntries", (it) => { afterEach(() => { vi.restoreAllMocks(); }); describe("list", () => { it.effect("returns the complete cached workspace index", () => Effect.gen(function* () { const cwd = yield* makeTempDir(); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "README.md"); yield* writeTextFile(cwd, "node_modules/pkg/index.js"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.list({ cwd }); expect(result.entries).toEqual( expect.arrayContaining([ { path: "src", kind: "directory" }, { path: "src/components", kind: "directory" }, { path: "src/components/Composer.tsx", kind: "file", }, { path: "README.md", kind: "file" }, ]), ); expect(result.entries.some((entry) => entry.path.startsWith("node_modules"))).toBe(false); expect(result.truncated).toBe(false); }), ); }); 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("supports typo-resistant file search through fff", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fff-typo-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); const result = yield* searchWorkspaceEntries({ cwd, query: "compoesr", limit: 10 }); expect(result.entries).toEqual( expect.arrayContaining([ expect.objectContaining({ path: "src/components/Composer.tsx" }), ]), ); }), ); it.effect("rebuilds the cached index after refresh fails", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-refresh-failure-" }); yield* writeTextFile(cwd, "src/index.ts", "export {};\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const createSpy = vi.spyOn(FileFinder, "create"); yield* workspaceEntries.list({ cwd }); expect(createSpy).toHaveBeenCalledTimes(1); vi.spyOn(FileFinder.prototype, "scanFiles").mockReturnValueOnce({ ok: false, error: "scan failed", }); yield* workspaceEntries.refresh(cwd); yield* workspaceEntries.list({ cwd }); expect(createSpy).toHaveBeenCalledTimes(2); }), ); }); describe("browse", () => { it.effect("returns matching directories and excludes files", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries.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.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 cwdWithSeparator = yield* appendSeparator(cwd); const directoryResult = yield* workspaceEntries.browse({ partialPath: cwdWithSeparator, }); const hiddenPrefixResult = yield* workspaceEntries.browse({ partialPath: `${cwdWithSeparator}.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.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.WorkspaceEntries; const error = yield* workspaceEntries .browse({ partialPath: "./src", }) .pipe(Effect.flip); expect(error._tag).toBe("WorkspaceEntriesCurrentProjectRequiredError"); expect(error.message).toBe( "A current project is required to browse relative workspace path './src'.", ); }), ); it.effect("returns an empty listing when the OS denies directory access", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-eacces-" }); const denied = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" }); vi.mocked(NodeFSP.readdir).mockRejectedValueOnce(denied); const result = yield* workspaceEntries.browse({ partialPath: yield* appendSeparator(cwd), }); expect(result).toEqual({ parentPath: cwd, entries: [] }); }), ); });});
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFSP from "node:fs/promises";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { FileFinder } from "@ff-labs/fff-node";
import { it, afterEach, describe, expect } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import { vi } from "vite-plus/test";
import * as ServerConfig from "../config.ts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as VcsProcess from "../vcs/VcsProcess.ts";
import * as WorkspaceEntries from "./WorkspaceEntries.ts";
import * as WorkspacePaths from "./WorkspacePaths.ts";
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return { ...actual, readdir: vi.fn(actual.readdir) };
});
const TestLayer = Layer.empty.pipe(
Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))),
Layer.provideMerge(WorkspacePaths.layer),
Layer.provideMerge(VcsProcess.layer),
Layer.provide(
ServerConfig.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 dir = yield* fileSystem.makeTempDirectoryScoped({
prefix: opts?.prefix ?? "t3code-workspace-entries-",
});
if (opts?.git) {
yield* git(dir, ["init"]);
}
return dir;
});
function writeTextFile(
cwd: string,
relativePath: string,
contents = "",
): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> {
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<string>, env?: NodeJS.ProcessEnv) =>
Effect.gen(function* () {
const process = yield* VcsProcess.VcsProcess;
const result = yield* process.run({
operation: "WorkspaceEntries.test.git",
command: "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.WorkspaceEntries;
return yield* workspaceEntries.search(input);
});
const appendSeparator = (input: string) =>
Effect.map(HostProcessPlatform, (platform) =>
input.endsWith("/") || input.endsWith("\\")
? input
: `${input}${platform === "win32" ? "\\" : "/"}`,
);
it.layer(TestLayer, { excludeTestServices: true })("WorkspaceEntries", (it) => {
afterEach(() => {
vi.restoreAllMocks();
});
describe("list", () => {
it.effect("returns the complete cached workspace index", () =>
Effect.gen(function* () {
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "src/components/Composer.tsx");
yield* writeTextFile(cwd, "README.md");
yield* writeTextFile(cwd, "node_modules/pkg/index.js");
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const result = yield* workspaceEntries.list({ cwd });
expect(result.entries).toEqual(
expect.arrayContaining([
{ path: "src", kind: "directory" },
{ path: "src/components", kind: "directory" },
{
path: "src/components/Composer.tsx",
kind: "file",
},
{ path: "README.md", kind: "file" },
]),
);
expect(result.entries.some((entry) => entry.path.startsWith("node_modules"))).toBe(false);
expect(result.truncated).toBe(false);
}),
);
});
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("supports typo-resistant file search through fff", () =>
Effect.gen(function* () {
const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fff-typo-" });
yield* writeTextFile(cwd, "src/components/Composer.tsx");
const result = yield* searchWorkspaceEntries({ cwd, query: "compoesr", limit: 10 });
expect(result.entries).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: "src/components/Composer.tsx" }),
]),
);
}),
);
it.effect("rebuilds the cached index after refresh fails", () =>
Effect.gen(function* () {
const cwd = yield* makeTempDir({ prefix: "t3code-workspace-refresh-failure-" });
yield* writeTextFile(cwd, "src/index.ts", "export {};\n");
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const createSpy = vi.spyOn(FileFinder, "create");
yield* workspaceEntries.list({ cwd });
expect(createSpy).toHaveBeenCalledTimes(1);
vi.spyOn(FileFinder.prototype, "scanFiles").mockReturnValueOnce({
ok: false,
error: "scan failed",
});
yield* workspaceEntries.refresh(cwd);
yield* workspaceEntries.list({ cwd });
expect(createSpy).toHaveBeenCalledTimes(2);
}),
);
});
describe("browse", () => {
it.effect("returns matching directories and excludes files", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.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.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 cwdWithSeparator = yield* appendSeparator(cwd);
const directoryResult = yield* workspaceEntries.browse({
partialPath: cwdWithSeparator,
});
const hiddenPrefixResult = yield* workspaceEntries.browse({
partialPath: `${cwdWithSeparator}.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.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.WorkspaceEntries;
const error = yield* workspaceEntries
.browse({
partialPath: "./src",
})
.pipe(Effect.flip);
expect(error._tag).toBe("WorkspaceEntriesCurrentProjectRequiredError");
expect(error.message).toBe(
"A current project is required to browse relative workspace path './src'.",
);
}),
);
it.effect("returns an empty listing when the OS denies directory access", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-eacces-" });
const denied = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
vi.mocked(NodeFSP.readdir).mockRejectedValueOnce(denied);
const result = yield* workspaceEntries.browse({
partialPath: yield* appendSeparator(cwd),
});
expect(result).toEqual({ parentPath: cwd, entries: [] });
}),
);
});
});