fix(core): align grep behavior and guidance (#38999)

This commit is contained in:
Aiden Cline 2026-07-26 17:29:27 -05:00 committed by GitHub
commit 0fd73a2976
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 128 additions and 25 deletions

View file

@ -37,7 +37,14 @@ const globToolNode = makeLocationNode({
const grepToolNode = makeLocationNode({
name: "test/grep-tool-plugin",
layer: Layer.effectDiscard(registerToolPlugin(GrepTool.Plugin)),
deps: [ToolRegistry.toolsNode, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node],
deps: [
ToolRegistry.toolsNode,
FSUtil.node,
Ripgrep.node,
Location.node,
LocationMutation.node,
PermissionV2.node,
],
})
const sessionID = SessionV2.ID.make("ses_search_tool_test")
@ -183,6 +190,94 @@ describe("search tools", () => {
),
)
it.live("reports no grep matches", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.promise(() => fs.writeFile(path.join(tmp.path, "file.txt"), "haystack\n")).pipe(
Effect.andThen(
withTools(tmp.path, (registry) => executeTool(registry, call("grep", { pattern: "needle" }))),
),
Effect.tap((result) =>
Effect.sync(() => {
expect(result).toMatchObject({
status: "completed",
content: [{ type: "text", text: "No matches found" }],
metadata: { matches: 0, truncated: false },
})
}),
),
),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("reports invalid grep regex details", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
withTools(tmp.path, (registry) =>
Effect.gen(function* () {
const result = yield* executeTool(registry, call("grep", { pattern: "[" }))
expect(result).toMatchObject({
status: "error",
error: { type: "tool.execution" },
})
if (result.status !== "error") return
expect(result.error.message).toStartWith("Invalid regex pattern:")
expect(result.error.message).toContain("unclosed character class")
}),
),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("requires external_directory approval for external grep files and directories", () =>
Effect.acquireUseRelease(
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
([active, outside]) => {
const assertions: PermissionV2.AssertInput[] = []
return Effect.promise(() => fs.writeFile(path.join(outside.path, "outside.txt"), "needle\n")).pipe(
Effect.andThen(
withTools(
active.path,
(registry) =>
Effect.gen(function* () {
const directory = yield* executeTool(
registry,
call("grep", { path: outside.path, pattern: "needle" }),
)
const file = yield* executeTool(
registry,
call("grep", { path: path.join(outside.path, "outside.txt"), pattern: "needle" }),
)
expect(directory.status).toBe("completed")
expect(file.status).toBe("completed")
}),
assertions,
),
),
Effect.tap(() =>
Effect.sync(() => {
expect(assertions.map((input) => input.action)).toEqual([
"external_directory",
"grep",
"external_directory",
"grep",
])
expect(assertions[0]?.resources).toEqual([path.join(outside.path, "*").replaceAll("\\", "/")])
expect(assertions[2]?.resources).toEqual([path.join(outside.path, "*").replaceAll("\\", "/")])
}),
),
)
},
([active, outside]) =>
Effect.promise(() =>
Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined),
),
),
)
for (const name of ["glob", "grep"] as const) {
it.live(`${name} reports a missing search path`, () =>
Effect.acquireUseRelease(