Files
paseo/packages/server/src/utils/directory-suggestions.test.ts
T
Mohamed Boudra a5c566f807 Make full CI finish sooner: eight browser shards, split Windows server job, cancel superseded runs (#7275)
Eight Playwright shards with two workers each, format/lint/relay/sdk combined into one routed quality job, Windows server integration in its own job, cancel-in-progress on every ref, one vitest start for the filtered integration files, no fan-in gate, and a deadline for the heap-bound directory search test that covers its 27,000-directory fixture on Windows.

First run on the Team plan: 19m48s from creation to last job with zero queue, all 21 jobs green, 224 runner-minutes. The same workflow under the old 20-job cap finished in 58 minutes with up to 40 minutes of queue per job.
2026-10-18 13:52:54 +07:00

1284 lines
42 KiB
TypeScript

import { createHook } from "node:async_hooks";
import { execFileSync, spawn, type ChildProcess } 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 { startPathContainmentMetrics, stopPathContainmentMetrics } from "./path.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.skipIf(isWindows)(
"prunes a symlink whose target is inside a Git-ignored directory",
async () => {
initGitRepo(searchRoot, "generated/\n");
mkdirSync(path.join(searchRoot, "generated", "output"), { recursive: true });
symlinkSync(path.join(searchRoot, "generated", "output"), path.join(searchRoot, "linked"));
await expect(
searchDirectoryEntries({
root: searchRoot,
query: "linked",
pathFormat: "relative",
includeFiles: false,
includeDirectories: true,
respectGitIgnore: true,
}),
).resolves.toEqual([]);
},
);
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("home search with excluded discovery paths", () => {
let homeDir: string;
let libraryDir: string;
beforeEach(() => {
homeDir = realpathSync.native(mkdtempSync(path.join(tmpdir(), "directory-excluded-")));
libraryDir = path.join(homeDir, "Library");
mkdirSync(path.join(libraryDir, "sub", "match-me"), { recursive: true });
mkdirSync(path.join(homeDir, "projects", "other"), { recursive: true });
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
});
function searchHome(query: string) {
return searchDirectoryEntries({
root: homeDir,
query,
pathFormat: "absolute",
includeDirectories: true,
includeFiles: false,
pathQueryPolicy: "rooted",
rootAliases: ["~"],
blankQueryBehavior: "none",
confidentResultScanThreshold: 5_000,
excludedDiscoveryPaths: [libraryDir],
}).then((entries) => entries.map((entry) => entry.path));
}
it("does not discover entries inside an excluded path", async () => {
await expect(searchHome("match")).resolves.toEqual([]);
await expect(searchHome("~")).resolves.toEqual([path.join(homeDir, "projects")]);
});
it("excludes by path, so a nested directory with the same name is still discovered", async () => {
const nested = path.join(homeDir, "projects", "game", "Library", "match-me");
mkdirSync(nested, { recursive: true });
await expect(searchHome("match")).resolves.toEqual([nested]);
});
// POSIX-only: creates and follows a symlink fixture.
it.skipIf(isWindows)("does not discover an excluded path through a symlink", async () => {
symlinkSync(path.join(libraryDir, "sub"), path.join(homeDir, "link"), "dir");
await expect(searchHome("match")).resolves.toEqual([]);
});
it("resolves an exact typed path inside an excluded path", async () => {
await expect(searchHome("~/Library/sub/match-me")).resolves.toEqual([
path.join(libraryDir, "sub", "match-me"),
]);
});
it("lists one level when browsing inside an excluded path", async () => {
await expect(searchHome("~/Library/")).resolves.toEqual([
libraryDir,
path.join(libraryDir, "sub"),
]);
await expect(searchHome("~/Library/su")).resolves.toEqual([path.join(libraryDir, "sub")]);
});
});
describe("rooted queries with a typed parent", () => {
let homeDir: string;
beforeEach(() => {
homeDir = realpathSync.native(mkdtempSync(path.join(tmpdir(), "directory-typed-parent-")));
mkdirSync(path.join(homeDir, "Developer", "pasta"), { recursive: true });
mkdirSync(path.join(homeDir, "Archive", "Developer", "pasta"), { recursive: true });
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
});
it("walks only under the typed parent when it exists", async () => {
await expect(
searchAbsoluteDirectoryPaths({ homeDir, query: "~/Developer/pas" }),
).resolves.toEqual([path.join(homeDir, "Developer", "pasta")]);
});
it("matches the whole tree when the typed parent does not exist", async () => {
await expect(searchAbsoluteDirectoryPaths({ homeDir, query: "~/Devel/pas" })).resolves.toEqual([
path.join(homeDir, "Developer", "pasta"),
path.join(homeDir, "Archive", "Developer", "pasta"),
]);
});
it("returns the typed directory first", async () => {
mkdirSync(path.join(homeDir, "Developer", "pasta-extra"));
const results = await searchAbsoluteDirectoryPaths({ homeDir, query: "~/Developer/pasta" });
expect(results[0]).toBe(path.join(homeDir, "Developer", "pasta"));
});
});
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", "search-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, "plugins", "usage-sources", "providers", "local.ts");
mkdirSync(path.dirname(targetPath), { recursive: true });
writeFileSync(targetPath, "");
const results = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "plugins/usage-sources/providers/local.ts",
limit: 20,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
maxEntriesScanned: 1,
});
expect(results).toEqual([
{
path: "plugins/usage-sources/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", "search-notes.md"), "generated notes\n");
const exactResults = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "generated/search-notes.md",
limit: 1,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
respectGitIgnore: true,
});
const fuzzyResults = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "search-notes",
limit: 20,
includeFiles: true,
includeDirectories: false,
respectGitIgnore: true,
});
expect({ exactResults, fuzzyResults }).toEqual({
exactResults: [{ path: "generated/search-notes.md", kind: "file" }],
fuzzyResults: [{ path: "docs/search-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/search-notes.md",
limit: 1,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
});
expect({ escaping, staysInside }).toEqual({
escaping: [],
staysInside: [{ path: "inside-link/search-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" },
{ path: "src/components/chat-input.tsx", kind: "file" },
]);
});
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" }]);
});
});
interface SearchProcessExit {
code: number | null;
signal: string | null;
}
interface SearchProcess {
child: ChildProcess;
exited: Promise<SearchProcessExit>;
}
// Reading a tree is cheap; re-deriving each entry's path state is not. A scan that asks whether
// every ancestor of every entry is inside the root, or is Git-ignored, costs multiples of the
// read and is what made large home directories exceed the client request timeout.
describe("home-tree scan cost", () => {
const DEPTH = 8;
const CHAINS = 40;
let scanRoot: string;
let searchProcess: SearchProcess | null = null;
beforeEach(() => {
scanRoot = realpathSync.native(mkdtempSync(path.join(tmpdir(), "directory-scan-cost-")));
for (let chain = 0; chain < CHAINS; chain += 1) {
const segments: string[] = [];
for (let level = 0; level < DEPTH; level += 1) segments.push(`c${chain}l${level}`);
mkdirSync(path.join(scanRoot, ...segments), { recursive: true });
}
});
afterEach(async () => {
try {
if (searchProcess) {
searchProcess.child.kill();
await searchProcess.exited;
}
} finally {
searchProcess = null;
rmSync(scanRoot, { recursive: true, force: true });
}
});
// Nothing matches, so the scan walks the whole tree instead of stopping at a confident result.
function scanWholeTree() {
return searchAbsoluteDirectoryPaths({
homeDir: scanRoot,
query: "nomatch",
maxDepth: DEPTH + 4,
});
}
async function countContainmentChecks(run: () => Promise<unknown>): Promise<number> {
startPathContainmentMetrics();
await run();
return stopPathContainmentMetrics();
}
// Creating the 27,000-directory fixture alone takes about 20 s on Windows
// runners under parallel load, and it has crossed the default 30 s deadline.
// The heap bound is enforced by the child's --max-old-space-size and its own
// 20 s timeout, so the test deadline only has to cover fixture setup.
it("finishes overlapping broad-tree searches within a bounded heap", async () => {
const broadRoot = path.join(scanRoot, "broad");
mkdirSync(broadRoot);
for (let branch = 0; branch < 30; branch += 1) {
const branchRoot = path.join(broadRoot, `branch-${branch}`);
mkdirSync(branchRoot);
for (let group = 0; group < 30; group += 1) {
const groupRoot = path.join(branchRoot, `group-${group}`);
mkdirSync(groupRoot);
for (let leaf = 0; leaf < 30; leaf += 1) {
mkdirSync(path.join(groupRoot, `leaf-${leaf}`));
}
}
}
// A smaller real tree and explicit heap budget keep the million-directory repro cheap in CI.
const moduleUrl = new URL("./directory-suggestions.ts", import.meta.url).href;
const source = `
const { searchDirectoryEntries } = await import(${JSON.stringify(moduleUrl)});
const results = await Promise.all(Array.from({ length: 14 }, (_, index) =>
searchDirectoryEntries({
root: ${JSON.stringify(broadRoot)},
query: "z".repeat(index + 1),
pathFormat: "absolute",
includeFiles: false,
includeDirectories: true,
pathQueryPolicy: "rooted",
rootAliases: ["~"],
blankQueryBehavior: "none",
confidentResultScanThreshold: 5000,
maxEntriesScanned: 2000,
limit: 30,
})
));
process.stdout.write(JSON.stringify(results));
`;
const child = spawn(
process.execPath,
[
"--max-old-space-size=128",
"--conditions=source",
"--import",
"tsx",
"--input-type=module",
"--eval",
source,
],
{ stdio: ["ignore", "pipe", "pipe"], timeout: 20_000 },
);
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdout += chunk;
});
child.stderr.on("data", (chunk: string) => {
stderr += chunk;
});
const exited = new Promise<SearchProcessExit>((resolve, reject) => {
child.on("error", reject);
child.on("close", (code, signal) => resolve({ code, signal }));
});
searchProcess = { child, exited };
const exit = await exited;
expect({ ...exit, outOfMemory: stderr.includes("heap out of memory") }).toEqual({
code: 0,
signal: null,
outOfMemory: false,
});
expect(JSON.parse(stdout)).toEqual(Array.from({ length: 14 }, () => []));
}, 120_000);
it("derives no containment for a tree that cannot leave the root", async () => {
const checks = await countContainmentChecks(scanWholeTree);
expect({ checks, scanned: CHAINS * DEPTH }).toEqual({ checks: 0, scanned: 320 });
});
it.skipIf(isWindows)(
"derives containment for symlinked entries, which can leave the root",
async () => {
const outside = realpathSync.native(mkdtempSync(path.join(tmpdir(), "directory-scan-away-")));
symlinkSync(outside, path.join(scanRoot, "escape"), "dir");
const checks = await countContainmentChecks(scanWholeTree);
expect({ checked: checks > 0, escaped: (await scanWholeTree()).includes(outside) }).toEqual({
checked: true,
escaped: false,
});
rmSync(outside, { recursive: true, force: true });
},
);
});
// Every pending fs call holds one of libuv's shared threadpool threads (4 by default). A read that
// never returns, such as one on a hung network mount, keeps its thread, so searches that pile up
// while the user types must not be able to take every thread from the rest of the daemon.
describe("filesystem threads held by concurrent searches", () => {
const SEARCHES = 8;
let scanRoot: string;
beforeEach(() => {
scanRoot = realpathSync.native(mkdtempSync(path.join(tmpdir(), "directory-scan-threads-")));
for (let branch = 0; branch < 20; branch += 1) {
mkdirSync(path.join(scanRoot, `b${branch}`, "inner", "leaf"), { recursive: true });
}
});
afterEach(() => {
rmSync(scanRoot, { recursive: true, force: true });
});
async function peakPendingFilesystemRequests(run: () => Promise<unknown>): Promise<number> {
const pending = new Set<number>();
let peak = 0;
const hook = createHook({
init(asyncId, type) {
if (type !== "FSREQPROMISE" && type !== "FSREQCALLBACK") return;
pending.add(asyncId);
peak = Math.max(peak, pending.size);
},
before(asyncId) {
pending.delete(asyncId);
},
});
hook.enable();
try {
await run();
} finally {
hook.disable();
}
return peak;
}
function searchWhileTyping() {
const queries = Array.from({ length: SEARCHES }, (_, index) => `nomatch${index}`);
return Promise.all(
queries.map((query) => searchAbsoluteDirectoryPaths({ homeDir: scanRoot, query })),
);
}
it("leaves threadpool threads free for the rest of the daemon", async () => {
const peak = await peakPendingFilesystemRequests(searchWhileTyping);
expect(peak).toBeLessThanOrEqual(2);
});
// Searches over one tree walk it in the same order, so searches stuck on one unreadable folder
// must hold one thread between them and leave the other for unrelated searches.
it("shares one filesystem request among searches waiting on the same path", async () => {
const peak = await peakPendingFilesystemRequests(searchWhileTyping);
expect(peak).toBe(1);
});
});