Files
paseo/packages/server/src/utils/directory-suggestions.test.ts
T
Mohamed Boudra e55dd5a212 fix(search): match complete paths in file and project search
Treat slash-separated fragments as searchable text and use the same full-path fuzzy matcher for daemon results and remembered project paths.
2026-08-19 12:18:23 +02:00

953 lines
30 KiB
TypeScript

import { execFileSync } from "node:child_process";
import {
mkdtempSync,
mkdirSync,
realpathSync,
rmSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { isPlatform } from "../test-utils/platform.js";
import { startGitCommandMetrics, stopGitCommandMetrics } from "./run-git-command.js";
import {
searchDirectoryEntries,
WORKSPACE_SEARCH_HIDDEN_DIRECTORIES,
} from "./directory-suggestions.js";
const isWindows = isPlatform("win32");
const filesystemRootDirectoryName = isWindows ? "Windows" : "usr";
async function searchAbsoluteDirectoryPaths(options: {
homeDir: string;
query: string;
limit?: number;
maxDepth?: number;
maxDirectoriesScanned?: number;
}): Promise<string[]> {
const entries = await searchDirectoryEntries({
root: options.homeDir,
query: options.query,
pathFormat: "absolute",
includeDirectories: true,
includeFiles: false,
pathQueryPolicy: "rooted",
rootAliases: ["~"],
blankQueryBehavior: "none",
limit: options.limit,
maxDepth: options.maxDepth,
maxEntriesScanned: options.maxDirectoriesScanned,
confidentResultScanThreshold: 5_000,
});
return entries.map((entry) => entry.path);
}
async function searchRelativeDirectoryEntries(options: {
cwd: string;
query: string;
limit?: number;
includeFiles?: boolean;
includeDirectories?: boolean;
matchMode?: "fuzzy" | "suffix";
maxDepth?: number;
maxEntriesScanned?: number;
respectGitIgnore?: boolean;
}) {
return searchDirectoryEntries({
root: options.cwd,
query: options.query,
pathFormat: "relative",
includeFiles: options.includeFiles,
includeDirectories: options.includeDirectories,
matchMode: options.matchMode,
pathQueryPolicy: "slashes",
blankQueryBehavior: "children",
traversableHiddenDirectoryNames: WORKSPACE_SEARCH_HIDDEN_DIRECTORIES,
limit: options.limit,
maxDepth: options.maxDepth,
maxEntriesScanned: options.maxEntriesScanned,
respectGitIgnore: options.respectGitIgnore,
});
}
function initGitRepo(directory: string, ignorePatterns: string): void {
execFileSync("git", ["init", "-q"], { cwd: directory });
writeFileSync(path.join(directory, ".gitignore"), ignorePatterns);
}
describe("searchDirectoryEntries", () => {
let configuredSearchRoot: string;
let searchRoot: string;
beforeEach(() => {
configuredSearchRoot = mkdtempSync(path.join(tmpdir(), "directory-search-"));
searchRoot = realpathSync.native(configuredSearchRoot);
mkdirSync(path.join(searchRoot, "projects", "paseo-desktop"), { recursive: true });
mkdirSync(path.join(searchRoot, "src", "components"), { recursive: true });
mkdirSync(path.join(searchRoot, ".hidden", "secret"), { recursive: true });
writeFileSync(path.join(searchRoot, "src", "components", "message-renderer.tsx"), "");
});
afterEach(() => {
rmSync(searchRoot, { recursive: true, force: true });
});
it("applies result paths and entry kinds as parameters of one search", async () => {
const directories = await searchDirectoryEntries({
root: configuredSearchRoot,
query: "pso",
pathFormat: "absolute",
includeFiles: false,
includeDirectories: true,
});
const files = await searchDirectoryEntries({
root: searchRoot,
query: "msgrndr",
pathFormat: "relative",
includeFiles: true,
includeDirectories: false,
});
expect({ directories, files }).toEqual({
directories: [
{
path: path.join(searchRoot, "projects", "paseo-desktop"),
kind: "directory",
},
],
files: [
{
path: "src/components/message-renderer.tsx",
kind: "file",
},
],
});
});
it("prunes directories ignored by Git and caches concurrent lookups", async () => {
execFileSync("git", ["init", "-q"], { cwd: searchRoot });
writeFileSync(path.join(searchRoot, ".gitignore"), "data/\n");
mkdirSync(path.join(searchRoot, "data", "matching-ignored-directory"), {
recursive: true,
});
mkdirSync(path.join(searchRoot, "visible", "matching-visible-directory"), {
recursive: true,
});
startGitCommandMetrics();
const results = await Promise.all([
searchDirectoryEntries({
root: searchRoot,
query: "matching",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
respectGitIgnore: true,
}),
searchDirectoryEntries({
root: searchRoot,
query: "visible",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
respectGitIgnore: true,
}),
]);
const gitMetrics = stopGitCommandMetrics();
expect(results).toEqual([
[{ path: "visible/matching-visible-directory", kind: "directory" }],
[
{ path: "visible", kind: "directory" },
{ path: "visible/matching-visible-directory", kind: "directory" },
],
]);
expect(gitMetrics.commands.filter((command) => command.args.includes("ls-files"))).toHaveLength(
1,
);
});
it("configures raw blank queries independently from explicit root aliases", async () => {
const rootEntries = [
{ path: "projects", kind: "directory" as const },
{ path: "src", kind: "directory" as const },
];
const common = {
root: searchRoot,
pathFormat: "relative" as const,
includeFiles: false,
includeDirectories: true,
rootAliases: ["~"],
};
await expect(
searchDirectoryEntries({
...common,
query: "",
blankQueryBehavior: "none",
}),
).resolves.toEqual([]);
await expect(
searchDirectoryEntries({
...common,
query: "~",
blankQueryBehavior: "none",
}),
).resolves.toEqual(rootEntries);
await expect(
searchDirectoryEntries({
...common,
query: "",
blankQueryBehavior: "children",
}),
).resolves.toEqual(rootEntries);
const suffixRootBrowses = await Promise.all([
searchDirectoryEntries({
...common,
query: "",
matchMode: "suffix",
blankQueryBehavior: "children",
}),
searchDirectoryEntries({
...common,
query: "~",
matchMode: "suffix",
blankQueryBehavior: "none",
}),
]);
expect(suffixRootBrowses).toEqual([rootEntries, rootEntries]);
});
it("matches rooted queries against the full path at any depth", async () => {
mkdirSync(path.join(searchRoot, "nested", "pso-global"), { recursive: true });
mkdirSync(path.join(searchRoot, "pso-root"), { recursive: true });
const absoluteQuery = path.join(configuredSearchRoot, "pso");
const common = {
root: configuredSearchRoot,
pathFormat: "relative" as const,
includeFiles: false,
includeDirectories: true,
pathQueryPolicy: "rooted" as const,
rootAliases: ["~"],
};
const expected = [
{ path: "pso-root", kind: "directory" },
{ path: "nested/pso-global", kind: "directory" },
{ path: "projects/paseo-desktop", kind: "directory" },
];
await expect(searchDirectoryEntries({ ...common, query: "~/pso" })).resolves.toEqual(expected);
await expect(searchDirectoryEntries({ ...common, query: "./pso" })).resolves.toEqual(expected);
await expect(searchDirectoryEntries({ ...common, query: absoluteQuery })).resolves.toEqual(
expected,
);
});
it("browses an absolute root and an absolute directory ending in a separator", async () => {
const common = {
root: configuredSearchRoot,
pathFormat: "relative" as const,
includeFiles: false,
includeDirectories: true,
pathQueryPolicy: "rooted" as const,
};
const rootEntries = await searchDirectoryEntries({ ...common, query: configuredSearchRoot });
const projectEntries = await searchDirectoryEntries({
...common,
query: `${path.join(configuredSearchRoot, "projects")}${path.sep}`,
});
expect({ rootEntries, projectEntries }).toEqual({
rootEntries: [
{ path: "projects", kind: "directory" },
{ path: "src", kind: "directory" },
],
projectEntries: [
{ path: "projects", kind: "directory" },
{ path: "projects/paseo-desktop", kind: "directory" },
],
});
});
it("anchors single-segment absolute queries when the search root is a filesystem root", async () => {
const filesystemRoot = path.parse(searchRoot).root;
const exactPath = path.join(filesystemRoot, filesystemRootDirectoryName);
const incompletePath = exactPath.slice(0, -1);
const common = {
root: filesystemRoot,
pathFormat: "absolute" as const,
includeFiles: false,
includeDirectories: true,
pathQueryPolicy: "rooted" as const,
limit: 5,
};
await expect(searchDirectoryEntries({ ...common, query: incompletePath })).resolves.toEqual([]);
const exactEntries = await searchDirectoryEntries({ ...common, query: exactPath });
expect(exactEntries[0]).toEqual({ path: exactPath, kind: "directory" });
expect(
exactEntries.every(
(entry) => entry.path === exactPath || entry.path.startsWith(`${exactPath}${path.sep}`),
),
).toBe(true);
});
it("does not return entries below the configured traversal depth", async () => {
await expect(
searchDirectoryEntries({
root: searchRoot,
query: "message-renderer",
pathFormat: "relative",
includeFiles: true,
includeDirectories: false,
maxDepth: 2,
}),
).resolves.toEqual([]);
});
it("does not spend the scan budget on excluded entry kinds", async () => {
const budgetRoot = path.join(searchRoot, "kind-budget");
const target = path.join(budgetRoot, "z-projects", "paseo-target");
mkdirSync(target, { recursive: true });
for (let index = 0; index < 10; index += 1) {
writeFileSync(path.join(budgetRoot, `a-noise-${index}.txt`), "");
}
await expect(
searchDirectoryEntries({
root: budgetRoot,
query: "paseo-target",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
maxEntriesScanned: 2,
}),
).resolves.toEqual([{ path: "z-projects/paseo-target", kind: "directory" }]);
});
it("applies ignored-directory policy to parent-scoped queries", async () => {
mkdirSync(path.join(searchRoot, "node_modules"));
await expect(
searchDirectoryEntries({
root: searchRoot,
query: "./node",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
}),
).resolves.toEqual([]);
});
it("does not traverse dependency, environment, or build-output directories", async () => {
const ignoredNames = [
"node_modules",
"venv",
"env",
"virtualenv",
"dist",
"build",
"target",
"out",
"coverage",
"vendor",
"__pycache__",
".git",
];
for (const name of ignoredNames) {
const ignoredTarget = path.join(searchRoot, name, "search-target.ts");
mkdirSync(path.dirname(ignoredTarget), { recursive: true });
writeFileSync(ignoredTarget, "");
}
const visibleTarget = path.join(searchRoot, "src", "search-target.ts");
writeFileSync(visibleTarget, "");
await expect(
searchDirectoryEntries({
root: searchRoot,
query: "search-target.ts",
pathFormat: "relative",
includeFiles: true,
includeDirectories: false,
}),
).resolves.toEqual([{ path: "src/search-target.ts", kind: "file" }]);
});
it.skipIf(isWindows)("rechecks cached symlink children against each search root", async () => {
const narrowRoot = path.join(searchRoot, "narrow");
const outsideNarrowRoot = path.join(searchRoot, "outside");
mkdirSync(narrowRoot, { recursive: true });
mkdirSync(path.join(outsideNarrowRoot, "leaked-child"), { recursive: true });
symlinkSync(outsideNarrowRoot, path.join(narrowRoot, "outside-link"));
await searchDirectoryEntries({
root: searchRoot,
query: "leaked-child",
pathFormat: "absolute",
includeFiles: false,
includeDirectories: true,
});
await expect(
searchDirectoryEntries({
root: narrowRoot,
query: "outside",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
}),
).resolves.toEqual([]);
});
it.skipIf(isWindows)("rechecks a cached symlink after its target changes", async () => {
const narrowRoot = path.join(searchRoot, "retargeted-link-root");
const insideTarget = path.join(narrowRoot, "inside");
const outsideTarget = path.join(searchRoot, "retargeted-link-outside");
const link = path.join(narrowRoot, "project-link");
mkdirSync(insideTarget, { recursive: true });
mkdirSync(outsideTarget, { recursive: true });
symlinkSync(insideTarget, link);
await expect(
searchDirectoryEntries({
root: narrowRoot,
query: "project-link",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
}),
).resolves.toEqual([{ path: "project-link", kind: "directory" }]);
unlinkSync(link);
symlinkSync(outsideTarget, link);
await expect(
searchDirectoryEntries({
root: narrowRoot,
query: "project-link",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
}),
).resolves.toEqual([]);
});
it("refreshes a cached directory after a child is created", async () => {
const dynamicRoot = path.join(searchRoot, "dynamic-cache-root");
mkdirSync(dynamicRoot);
await expect(
searchDirectoryEntries({
root: dynamicRoot,
query: "fresh-project",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
}),
).resolves.toEqual([]);
mkdirSync(path.join(dynamicRoot, "fresh-project"));
await expect(
searchDirectoryEntries({
root: dynamicRoot,
query: "fresh-project",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
}),
).resolves.toEqual([{ path: "fresh-project", kind: "directory" }]);
});
});
describe("absolute directory-path configuration", () => {
let tempRoot: string;
let homeDir: string;
let outsideDir: string;
beforeEach(() => {
tempRoot = realpathSync.native(mkdtempSync(path.join(tmpdir(), "directory-suggestions-")));
homeDir = path.join(tempRoot, "home");
outsideDir = path.join(tempRoot, "outside");
mkdirSync(homeDir, { recursive: true });
mkdirSync(outsideDir, { recursive: true });
homeDir = realpathSync.native(homeDir);
outsideDir = realpathSync.native(outsideDir);
mkdirSync(path.join(homeDir, "projects", "paseo"), { recursive: true });
mkdirSync(path.join(homeDir, "projects", "playground"), { recursive: true });
mkdirSync(path.join(homeDir, "documents", "plans"), { recursive: true });
mkdirSync(path.join(homeDir, ".hidden", "cache"), { recursive: true });
writeFileSync(path.join(homeDir, "projects", "README.md"), "not a directory\n");
mkdirSync(path.join(outsideDir, "outside-match"), { recursive: true });
if (!isWindows) {
symlinkSync(path.join(outsideDir, "outside-match"), path.join(homeDir, "outside-link"));
}
});
afterEach(() => {
rmSync(tempRoot, { recursive: true, force: true });
});
it("does not inspect directories when the scan budget is zero", async () => {
await expect(
searchAbsoluteDirectoryPaths({
homeDir,
query: "documents",
limit: 10,
maxDirectoriesScanned: 0,
}),
).resolves.toEqual([]);
});
it("shares the scan budget fairly between nested sibling branches", async () => {
const budgetHome = path.join(tempRoot, "nested-budget-home");
const projectPath = path.join(budgetHome, "work", "client", "team", "paseo-desktop");
mkdirSync(projectPath, { recursive: true });
for (let index = 0; index < 10; index += 1) {
mkdirSync(
path.join(budgetHome, "work", "archive", `noise-${index.toString().padStart(2, "0")}`),
{ recursive: true },
);
}
const results = await searchAbsoluteDirectoryPaths({
homeDir: budgetHome,
query: "paseo-desktop",
limit: 10,
maxDirectoriesScanned: 8,
});
expect(results.map((result) => realpathSync.native(result))).toEqual([
realpathSync.native(projectPath),
]);
});
it.skipIf(isWindows)("does not let a queued symlink hide the direct project branch", async () => {
const symlinkHome = path.join(tempRoot, "symlink-budget-home");
const projectRoot = path.join(symlinkHome, "b-projects", "project-root");
const projectPath = path.join(projectRoot, "paseo-desktop");
const noisyBranch = path.join(symlinkHome, "a-noisy");
mkdirSync(projectPath, { recursive: true });
for (let index = 0; index < 10; index += 1) {
mkdirSync(path.join(noisyBranch, `noise-${index.toString().padStart(2, "0")}`), {
recursive: true,
});
}
// The alias is intentionally queued behind the noise. Discovery must not
// reserve its target before the direct b-projects branch gets a turn.
symlinkSync(projectRoot, path.join(noisyBranch, "zz-target-link"));
const results = await searchAbsoluteDirectoryPaths({
homeDir: symlinkHome,
query: "paseo-desktop",
limit: 10,
maxDirectoriesScanned: 6,
});
expect(results.map((result) => realpathSync.native(result))).toEqual([
realpathSync.native(projectPath),
]);
});
it.skipIf(isWindows)("follows visible directory symlinks that stay inside home", async () => {
const symlinkHome = path.join(tempRoot, "internal-symlink-home");
const projectPath = path.join(symlinkHome, ".linked", "project-root", "paseo-desktop");
mkdirSync(projectPath, { recursive: true });
symlinkSync(path.dirname(projectPath), path.join(symlinkHome, "linked-project"));
const results = await searchAbsoluteDirectoryPaths({
homeDir: symlinkHome,
query: "pso",
limit: 10,
});
expect(results.map((result) => realpathSync.native(result))).toEqual([
realpathSync.native(projectPath),
]);
});
it.skipIf(isWindows)("matches the visible name of a directory symlink", async () => {
const symlinkHome = path.join(tempRoot, "visible-symlink-home");
const projectsPath = path.join(symlinkHome, "projects");
const targetPath = path.join(symlinkHome, "work", "current");
const visibleProjectPath = path.join(projectsPath, "paseo");
mkdirSync(projectsPath, { recursive: true });
mkdirSync(targetPath, { recursive: true });
symlinkSync(targetPath, visibleProjectPath);
const results = await searchAbsoluteDirectoryPaths({
homeDir: symlinkHome,
query: "paseo",
limit: 10,
});
expect(results).toContain(visibleProjectPath);
});
it("keeps scanning past weak fuzzy matches for a stronger late result", async () => {
const largeHome = path.join(tempRoot, "large-home");
const exactMatchPath = path.join(largeHome, "pso");
for (let index = 0; index < 8; index += 1) {
mkdirSync(
path.join(largeHome, `a-${index.toString().padStart(2, "0")}-project-search-output`),
{ recursive: true },
);
}
mkdirSync(exactMatchPath);
const results = await searchDirectoryEntries({
root: largeHome,
query: "pso",
pathFormat: "absolute",
includeFiles: false,
includeDirectories: true,
limit: 1,
maxEntriesScanned: 20,
confidentResultScanThreshold: 5,
});
expect(results).toEqual([{ path: exactMatchPath, kind: "directory" }]);
});
it("supports home-relative path query syntax", async () => {
const result = await searchAbsoluteDirectoryPaths({
homeDir,
query: "~/projects/pa",
limit: 10,
});
expect(result.map((entry) => realpathSync.native(entry))).toEqual([
realpathSync.native(path.join(homeDir, "projects", "paseo")),
realpathSync.native(path.join(homeDir, "projects", "playground")),
]);
});
it("prioritizes partial matches that appear earlier in the path", async () => {
const earlierPath = path.join(homeDir, "farofoo");
const laterPath = path.join(homeDir, "x", "y", "farofoo");
mkdirSync(earlierPath, { recursive: true });
mkdirSync(laterPath, { recursive: true });
const results = await searchAbsoluteDirectoryPaths({
homeDir,
query: "arofo",
limit: 30,
});
const resolvedResults = results.map((result) => realpathSync.native(result));
const earlierIndex = resolvedResults.indexOf(realpathSync.native(earlierPath));
const laterIndex = resolvedResults.indexOf(realpathSync.native(laterPath));
expect(earlierIndex).toBeGreaterThanOrEqual(0);
expect(laterIndex).toBeGreaterThanOrEqual(0);
expect(earlierIndex).toBeLessThan(laterIndex);
});
// POSIX-only: creates and follows a symlink escape fixture.
it.skipIf(isWindows)("does not return paths that escape home through symlinks", async () => {
const results = await searchAbsoluteDirectoryPaths({
homeDir,
query: "outside",
limit: 20,
});
expect(results).not.toContain(path.join(homeDir, "outside-link"));
expect(results).not.toContain(path.join(outsideDir, "outside-match"));
});
it("respects the result limit", async () => {
const results = await searchAbsoluteDirectoryPaths({
homeDir,
query: "p",
limit: 1,
});
expect(results).toHaveLength(1);
});
});
describe("relative typed-entry configuration", () => {
let tempRoot: string;
let workspaceDir: string;
beforeEach(() => {
tempRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "workspace-suggestions-")));
workspaceDir = path.join(tempRoot, "workspace");
mkdirSync(path.join(workspaceDir, "src", "components"), {
recursive: true,
});
mkdirSync(path.join(workspaceDir, "docs"), { recursive: true });
writeFileSync(path.join(workspaceDir, "README.md"), "# paseo\n");
writeFileSync(
path.join(workspaceDir, "src", "components", "chat-input.tsx"),
"export const ChatInput = null;\n",
);
writeFileSync(path.join(workspaceDir, "docs", "notes.md"), "notes\n");
});
afterEach(() => {
rmSync(tempRoot, { recursive: true, force: true });
});
it("ranks fuzzy basename matches after exact, prefix, and substring matches", async () => {
writeFileSync(path.join(workspaceDir, "src", "components", "msgrndr"), "");
writeFileSync(path.join(workspaceDir, "src", "components", "msgrndr-panel.tsx"), "");
writeFileSync(path.join(workspaceDir, "src", "components", "use-msgrndr.ts"), "");
writeFileSync(path.join(workspaceDir, "src", "components", "message-renderer.tsx"), "");
const results = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "msgrndr",
limit: 20,
includeFiles: true,
includeDirectories: false,
});
expect(results.map((entry) => entry.path)).toEqual([
"src/components/msgrndr",
"src/components/msgrndr-panel.tsx",
"src/components/use-msgrndr.ts",
"src/components/message-renderer.tsx",
]);
});
it("suffix mode matches whole path segment suffixes without fuzzy matches", async () => {
mkdirSync(path.join(workspaceDir, "packages", "app", "src"), { recursive: true });
writeFileSync(path.join(workspaceDir, "src", "file.ts"), "");
writeFileSync(path.join(workspaceDir, "packages", "app", "src", "file.ts"), "");
writeFileSync(path.join(workspaceDir, "src", "paseo-config-file.ts"), "");
const basenameResults = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "file.ts",
limit: 20,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
});
const suffixResults = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "src/file.ts",
limit: 20,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
});
expect(basenameResults).toEqual([
{ path: "src/file.ts", kind: "file" },
{ path: "packages/app/src/file.ts", kind: "file" },
]);
expect(suffixResults).toEqual([
{ path: "src/file.ts", kind: "file" },
{ path: "packages/app/src/file.ts", kind: "file" },
]);
});
it("suffix mode resolves exact workspace file paths before broad traversal", async () => {
const targetPath = path.join(
workspaceDir,
"packages",
"server",
"src",
"services",
"quota-fetcher",
"providers",
"local.ts",
);
mkdirSync(path.dirname(targetPath), { recursive: true });
writeFileSync(targetPath, "");
const results = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "packages/server/src/services/quota-fetcher/providers/local.ts",
limit: 20,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
maxEntriesScanned: 1,
});
expect(results).toEqual([
{
path: "packages/server/src/services/quota-fetcher/providers/local.ts",
kind: "file",
},
]);
});
it("suffix mode resolves explicit hidden file paths without broad hidden traversal", async () => {
const targetPath = path.join(workspaceDir, ".dev", "paseo-home", "daemon.log");
mkdirSync(path.dirname(targetPath), { recursive: true });
writeFileSync(targetPath, "daemon log\n");
const results = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: ".dev/paseo-home/daemon.log",
limit: 20,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
maxEntriesScanned: 1,
});
expect(results).toEqual([{ path: ".dev/paseo-home/daemon.log", kind: "file" }]);
});
it("resolves an exact gitignored path while keeping it out of discovery results", async () => {
initGitRepo(workspaceDir, "generated/\n");
mkdirSync(path.join(workspaceDir, "generated"), { recursive: true });
writeFileSync(path.join(workspaceDir, "generated", "notes.md"), "generated notes\n");
const exactResults = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "generated/notes.md",
limit: 1,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
respectGitIgnore: true,
});
const fuzzyResults = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "notes",
limit: 20,
includeFiles: true,
includeDirectories: false,
respectGitIgnore: true,
});
expect({ exactResults, fuzzyResults }).toEqual({
exactResults: [{ path: "generated/notes.md", kind: "file" }],
fuzzyResults: [{ path: "docs/notes.md", kind: "file" }],
});
});
it.skipIf(isWindows)(
"refuses an exact path that escapes the root through a symlink",
async () => {
const outsideDir = path.join(tempRoot, "outside-workspace");
mkdirSync(outsideDir, { recursive: true });
writeFileSync(path.join(outsideDir, "secret.md"), "outside\n");
symlinkSync(outsideDir, path.join(workspaceDir, "escape-link"));
symlinkSync(path.join(workspaceDir, "docs"), path.join(workspaceDir, "inside-link"));
const escaping = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "escape-link/secret.md",
limit: 1,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
});
const staysInside = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "inside-link/notes.md",
limit: 1,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
});
expect({ escaping, staysInside }).toEqual({
escaping: [],
staysInside: [{ path: "inside-link/notes.md", kind: "file" }],
});
},
);
it("traverses only allowlisted hidden directories without suggesting the directories", async () => {
mkdirSync(path.join(workspaceDir, ".claude"), { recursive: true });
mkdirSync(path.join(workspaceDir, ".dev", "cache"), { recursive: true });
writeFileSync(path.join(workspaceDir, ".claude", "settings.local.json"), "{}");
writeFileSync(path.join(workspaceDir, ".dev", "cache", "settings.local.json"), "{}");
const suffixResults = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "settings.local.json",
limit: 20,
includeFiles: true,
includeDirectories: true,
matchMode: "suffix",
});
const fuzzyResults = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "claude",
limit: 20,
includeFiles: true,
includeDirectories: true,
matchMode: "fuzzy",
});
expect({ suffixResults, fuzzyResults }).toEqual({
suffixResults: [{ path: ".claude/settings.local.json", kind: "file" }],
fuzzyResults: [{ path: ".claude/settings.local.json", kind: "file" }],
});
});
it("supports slash-style path queries", async () => {
const results = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "src/co",
limit: 20,
includeFiles: true,
includeDirectories: true,
});
expect(results).toEqual([{ path: "src/components", kind: "directory" }]);
});
it("matches a path fragment anywhere in the full file path", async () => {
const targetPath = path.join(
workspaceDir,
"something",
"something-else",
"skills",
"paseo-advisor",
"SKILL.md",
);
mkdirSync(path.dirname(targetPath), { recursive: true });
writeFileSync(targetPath, "skill\n");
const results = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "skills/",
limit: 20,
includeFiles: true,
includeDirectories: false,
});
expect(results).toEqual([
{
path: "something/something-else/skills/paseo-advisor/SKILL.md",
kind: "file",
},
]);
});
it("fuzzy-matches natural language against the full path", async () => {
mkdirSync(path.join(workspaceDir, "blankpage", "editor"), { recursive: true });
const results = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "blank page editor",
limit: 20,
includeFiles: false,
includeDirectories: true,
});
expect(results).toEqual([{ path: "blankpage/editor", kind: "directory" }]);
});
});