fix(core): harden grep search behavior (#38922)

This commit is contained in:
Aiden Cline 2026-07-25 23:23:53 -05:00 committed by GitHub
commit 7affee529b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 81 additions and 9 deletions

View file

@ -104,7 +104,7 @@ describe("search tools", () => {
const grep = yield* executeTool(registry, call("grep", { pattern: "needle" }))
expect(glob.metadata).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT, truncated: true })
expect(grep.metadata).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT })
expect(grep.metadata).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT, truncated: true })
expect(glob.content).toHaveLength(1)
expect(grep.content).toHaveLength(1)
const globText = glob.content?.[0]?.type === "text" ? glob.content[0].text : ""
@ -114,6 +114,9 @@ describe("search tools", () => {
`(Results are truncated: showing first ${FileSystem.DEFAULT_SEARCH_LIMIT} results. Consider using a more specific path or pattern.)`,
)
expect(grepText).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`)
expect(grepText).toEndWith(
`(Results are truncated: showing first ${FileSystem.DEFAULT_SEARCH_LIMIT} results. Consider using a more specific path or pattern.)`,
)
}),
)
}),
@ -121,6 +124,65 @@ describe("search tools", () => {
),
)
it.live("rejects an empty grep pattern", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
withTools(tmp.path, (registry) =>
Effect.gen(function* () {
expect(yield* executeTool(registry, call("grep", { pattern: "" }))).toEqual({
status: "error",
error: {
type: "tool.execution",
message: 'Invalid tool input: Pattern must not be empty\n at ["pattern"]',
},
})
}),
),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("handles explicit grep file and directory paths", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.promise(() =>
Promise.all([
fs.writeFile(path.join(tmp.path, "target.txt"), "needle\n"),
fs.writeFile(path.join(tmp.path, "other.txt"), "needle\n"),
]),
).pipe(
Effect.andThen(
withTools(tmp.path, (registry) =>
Effect.gen(function* () {
const file = yield* executeTool(registry, call("grep", { path: "target.txt", pattern: "needle" }))
expect(file).toMatchObject({
status: "completed",
output: [{ entry: { path: "target.txt" }, line: 1, text: "needle\n" }],
metadata: { matches: 1, truncated: false },
})
const directory = yield* executeTool(registry, call("grep", { path: ".", pattern: "needle" }))
expect(directory).toMatchObject({
status: "completed",
metadata: { matches: 2, truncated: false },
})
if (directory.status !== "completed") return
expect(directory.output).toEqual(
expect.arrayContaining([
expect.objectContaining({ entry: expect.objectContaining({ path: "target.txt" }) }),
expect.objectContaining({ entry: expect.objectContaining({ path: "other.txt" }) }),
]),
)
}),
),
),
),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
for (const name of ["glob", "grep"] as const) {
it.live(`${name} reports a missing search path`, () =>
Effect.acquireUseRelease(