diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 82841581c6b..4f2317e0d82 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -173,6 +173,8 @@ const layer = Layer.effect( ...(input.hidden ? ["--hidden"] : []), ...(input.follow ? ["--follow"] : []), `--glob=${input.pattern}`, + // Positive globs override rg's hidden-file filter; exclude before applying the result limit. + ...(input.hidden ? [] : ["--glob=!**/.*"]), "--glob=!**/.git/**", ".", ], diff --git a/packages/core/test/ripgrep.test.ts b/packages/core/test/ripgrep.test.ts index 8937161ada6..c1cd4e19437 100644 --- a/packages/core/test/ripgrep.test.ts +++ b/packages/core/test/ripgrep.test.ts @@ -6,13 +6,43 @@ import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { Location } from "@opencode/core/location" import { Ripgrep } from "@opencode/core/ripgrep" import { RelativePath } from "@opencode/core/schema" -import { tmpdir } from "./fixture/tmpdir" +import { tmpdir, tmpdirScoped } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" import { tempLocationLayer } from "./fixture/location" const it = testEffect(AppNodeBuilder.build(Ripgrep.node, [Location.node.replace(tempLocationLayer)])) describe("Ripgrep", () => { + for (const hidden of [undefined, false, true]) { + for (const limit of hidden ? [10] : [1, 10]) { + it.live(`glob honors hidden=${hidden} before limit=${limit}`, () => + Effect.gen(function* () { + const tmp = yield* tmpdirScoped() + yield* Effect.promise(() => + Promise.all( + ["src/visible.ts", ".hidden.ts", "src/.hidden.ts", ".hidden/nested.ts", ".git/config.ts"].map((file) => + Bun.write(path.join(tmp.path, file), "needle\n"), + ), + ), + ) + const ripgrep = yield* Ripgrep.Service + const files = yield* ripgrep.glob({ + cwd: tmp.path, + pattern: "**/*.ts", + limit, + ...(hidden === undefined ? {} : { hidden }), + }) + + expect(files.map((item) => item.path).sort()).toEqual( + (hidden ? [".hidden.ts", ".hidden/nested.ts", "src/.hidden.ts", "src/visible.ts"] : ["src/visible.ts"]).map( + (file) => RelativePath.make(file), + ), + ) + }), + ) + } + } + it.live("globs files as an array", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 26314e46cf8..899984f50a4 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -17,7 +17,7 @@ import { GlobTool } from "@opencode/core/tool/plugin/glob" import { GrepTool } from "@opencode/core/tool/plugin/grep" import { Tool } from "@opencode/core/tool" import { location } from "./fixture/location" -import { tmpdir } from "./fixture/tmpdir" +import { tmpdir, tmpdirScoped } from "./fixture/tmpdir" import { it } from "./lib/effect" import { permissionLayer } from "./lib/permission" import { executeTool, registerToolPlugin, toolIdentity } from "./lib/tool" @@ -67,6 +67,45 @@ const call = (name: "glob" | "grep", input: unknown) => ({ }) describe("search tools", () => { + for (const hidden of [undefined, false, true]) { + for (const limit of hidden ? [10] : [1, 10]) { + it.live(`glob honors hidden=${hidden} before limit=${limit}`, () => + Effect.gen(function* () { + const tmp = yield* tmpdirScoped() + yield* Effect.promise(() => + Promise.all( + ["src/visible.ts", ".hidden.ts", "src/.hidden.ts", ".hidden/nested.ts", ".git/config.ts"].map((file) => + Bun.write(path.join(tmp.path, file), "needle\n"), + ), + ), + ) + yield* withTools(tmp.path, (registry) => + Effect.gen(function* () { + const result = yield* executeTool( + registry, + call("glob", { pattern: "**/*.ts", limit, ...(hidden === undefined ? {} : { hidden }) }), + ) + const expected = hidden + ? [".hidden.ts", ".hidden/nested.ts", "src/.hidden.ts", "src/visible.ts"] + : ["src/visible.ts"] + + expect(result.status).toBe("completed") + expect(result.output).toHaveLength(expected.length) + expect(result.output).toEqual( + expect.arrayContaining(expected.map((file) => ({ path: path.normalize(file), type: "file" }))), + ) + expect(result.metadata).toEqual({ count: expected.length, truncated: false }) + expect(result.content).toHaveLength(1) + expect(result.content?.[0]?.type === "text" ? result.content[0].text.split("\n").sort() : []).toEqual( + expected.map((file) => path.join(tmp.path, file)).sort(), + ) + }), + ) + }), + ) + } + } + it.live("bounds omitted glob and grep limits", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()),