Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion src/cli/find-path-cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { join, resolve } from "node:path";

interface CliResult {
stdout: string;
Expand Down Expand Up @@ -111,4 +111,55 @@ describe("search find --path", () => {
rmSync(dir, { recursive: true, force: true });
}
}, 60_000);

test("rejects root-name and root-id collisions plus an unconfigured filesystem path", async () => {
const dir = mkdtempSync(join(tmpdir(), "search-find-path-collisions-"));
const indexedRoot = join(dir, "indexed");
const unconfiguredRoot = join(dir, "unconfigured");
const collidingName = `path-name-collision-${Date.now()}`;
mkdirSync(indexedRoot, { recursive: true });
mkdirSync(unconfiguredRoot, { recursive: true });
writeFileSync(join(indexedRoot, "executor-collision.txt"), "executor must stay scoped\n");
const env = testEnv(dir);

try {
const added = await runCli(
["index", "add", indexedRoot, "--name", collidingName, "--json"],
env,
);
expect(added.exitCode).toBe(0);
const rootId = JSON.parse(added.stdout).root.id as string;

for (const ref of [collidingName, rootId]) {
const collision = await runCli(
["find", "executor", "--path", ref, "--json", "--no-refresh"],
env,
);
expect(collision.exitCode).not.toBe(0);
expect(collision.stdout).toBe("");
expect(collision.stderr).toContain(`Index root not found: ${resolve(ref)}`);
expect(collision.stderr).not.toContain(indexedRoot);

const genericRoot = await runCli(
["find", "executor", "--root", ref, "--json", "--no-refresh"],
env,
);
expect(genericRoot.exitCode).toBe(0);
expect(genericRoot.stderr).toBe("");
expect(JSON.parse(genericRoot.stdout).results).toEqual([
expect.objectContaining({ path: join(indexedRoot, "executor-collision.txt") }),
]);
}

const missing = await runCli(
["find", "executor", "--path", unconfiguredRoot, "--json", "--no-refresh"],
env,
);
expect(missing.exitCode).not.toBe(0);
expect(missing.stdout).toBe("");
expect(missing.stderr).toContain(`Index root not found: ${unconfiguredRoot}`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}, 60_000);
});
3 changes: 2 additions & 1 deletion src/cli/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ export function registerLocalCommands(program: Command): void {
try {
response = findLocal(query, {
kind: opts.kind as FindKind,
root: opts.path ?? opts.root,
root: opts.root,
rootPath: opts.path,
ext: opts.ext,
dir: opts.dir,
limit: parsePositiveInt(opts.limit, "--limit"),
Expand Down
28 changes: 21 additions & 7 deletions src/lib/local/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getRoot,
scheduleAutoRefreshStaleRoots,
listRoots,
normalizeRootPath,
rootHealth,
type IndexRoot,
type RootHealth,
Expand Down Expand Up @@ -34,6 +35,8 @@ export interface FindMatch {

export interface FindOptions extends LocalQueryOptions {
kind?: FindKind;
/** Limit to one configured root by normalized filesystem path only. */
rootPath?: string;
/** true refreshes synchronously, false skips refresh scheduling, undefined schedules async refresh. */
refresh?: boolean;
/** Treat the query as a regular expression (grep-style, line-based). */
Expand Down Expand Up @@ -78,11 +81,22 @@ export interface FindResponse {
* when a caller supplied a path. Once roots exist, a scoped query must name a
* real root and must report only that root in its population metadata.
*/
function queryRoots(rootRef: string | undefined, db?: Database): IndexRoot[] {
function queryRoots(
rootRef: string | undefined,
rootPath: string | undefined,
db?: Database,
): IndexRoot[] {
const roots = listRoots(db);
if (!rootRef || roots.length === 0) return roots;
if ((!rootRef && !rootPath) || roots.length === 0) return roots;

const root = getRoot(rootRef, db);
if (rootPath) {
const normalizedPath = normalizeRootPath(rootPath);
const root = roots.find((candidate) => candidate.path === normalizedPath);
if (!root) throw new Error(`Index root not found: ${normalizedPath}`);
return [root];
}

const root = getRoot(rootRef!, db);
if (!root) throw new Error(`Index root not found: ${rootRef}`);
return [root];
}
Expand Down Expand Up @@ -139,7 +153,7 @@ export function findLocal(query: string, opts: FindOptions = {}, db?: Database):
throw new Error(`Invalid kind "${kind}" — use file, content, or both.`);
}
const limit = clampLimit(opts.limit);
let roots = queryRoots(opts.root, db);
let roots = queryRoots(opts.root, opts.rootPath, db);

if (!hasReadyQueryRoot(roots)) {
// Kick recovery before giving up: a wedged root is exactly the case the
Expand All @@ -150,7 +164,7 @@ export function findLocal(query: string, opts: FindOptions = {}, db?: Database):
if (opts.refresh === true) autoRefreshStaleRoots(db);
else if (opts.refresh !== false) scheduleAutoRefreshStaleRoots(db);

roots = queryRoots(opts.root, db);
roots = queryRoots(opts.root, opts.rootPath, db);
if (hasReadyQueryRoot(roots)) return findLocal(query, { ...opts, refresh: false }, db);

return {
Expand All @@ -167,10 +181,10 @@ export function findLocal(query: string, opts: FindOptions = {}, db?: Database):

if (opts.refresh === true) autoRefreshStaleRoots(db);
else if (opts.refresh !== false) scheduleAutoRefreshStaleRoots(db);
roots = queryRoots(opts.root, db);
roots = queryRoots(opts.root, opts.rootPath, db);

const queryOpts: LocalQueryOptions = {
root: opts.root,
root: opts.rootPath ? roots[0]?.id : opts.root,
ext: opts.ext,
dir: opts.dir,
limit,
Expand Down
Loading