fix(core): report missing search paths (#35337)
This commit is contained in:
@@ -5,6 +5,7 @@ import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
|||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { FileSystem } from "../filesystem"
|
import { FileSystem } from "../filesystem"
|
||||||
|
import { FSUtil } from "../fs-util"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { Ripgrep } from "../ripgrep"
|
import { Ripgrep } from "../ripgrep"
|
||||||
import { RelativePath } from "../schema"
|
import { RelativePath } from "../schema"
|
||||||
@@ -36,6 +37,7 @@ export const toModelOutput = (output: ModelOutput) => {
|
|||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "core-glob-tool",
|
id: "core-glob-tool",
|
||||||
effect: Effect.fn("GlobTool.Plugin")(function* (ctx: PluginContext) {
|
effect: Effect.fn("GlobTool.Plugin")(function* (ctx: PluginContext) {
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
const ripgrep = yield* Ripgrep.Service
|
const ripgrep = yield* Ripgrep.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const permission = yield* PermissionV2.Service
|
const permission = yield* PermissionV2.Service
|
||||||
@@ -71,6 +73,13 @@ export const Plugin = {
|
|||||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||||
})
|
})
|
||||||
const cwd = path.resolve(location.directory, input.path ?? ".")
|
const cwd = path.resolve(location.directory, input.path ?? ".")
|
||||||
|
yield* fs
|
||||||
|
.stat(cwd)
|
||||||
|
.pipe(
|
||||||
|
Effect.catchReason("PlatformError", "NotFound", () =>
|
||||||
|
Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })),
|
||||||
|
),
|
||||||
|
)
|
||||||
return yield* ripgrep
|
return yield* ripgrep
|
||||||
.glob({
|
.glob({
|
||||||
cwd,
|
cwd,
|
||||||
@@ -88,7 +97,11 @@ export const Plugin = {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.mapError(() => new ToolFailure({ message: `Unable to find files matching ${input.pattern}` })),
|
Effect.mapError((error) =>
|
||||||
|
error instanceof ToolFailure
|
||||||
|
? error
|
||||||
|
: new ToolFailure({ message: `Unable to find files matching ${input.pattern}` }),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -91,7 +91,13 @@ export const Plugin = {
|
|||||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||||
})
|
})
|
||||||
const target = path.resolve(location.directory, input.path ?? ".")
|
const target = path.resolve(location.directory, input.path ?? ".")
|
||||||
const info = yield* fs.stat(target).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
const info = yield* fs
|
||||||
|
.stat(target)
|
||||||
|
.pipe(
|
||||||
|
Effect.catchReason("PlatformError", "NotFound", () =>
|
||||||
|
Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })),
|
||||||
|
),
|
||||||
|
)
|
||||||
return yield* ripgrep
|
return yield* ripgrep
|
||||||
.grep({
|
.grep({
|
||||||
cwd: info?.type === "Directory" ? target : path.dirname(target),
|
cwd: info?.type === "Directory" ? target : path.dirname(target),
|
||||||
@@ -121,7 +127,13 @@ export const Plugin = {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to grep for ${input.pattern}` }))),
|
}).pipe(
|
||||||
|
Effect.mapError((error) =>
|
||||||
|
error instanceof ToolFailure
|
||||||
|
? error
|
||||||
|
: new ToolFailure({ message: `Unable to grep for ${input.pattern}` }),
|
||||||
|
),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
|
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||||
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
|
import { GlobTool } from "@opencode-ai/core/tool/glob"
|
||||||
|
import { GrepTool } from "@opencode-ai/core/tool/grep"
|
||||||
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
|
import { location } from "./fixture/location"
|
||||||
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
import { testEffect } from "./lib/effect"
|
||||||
|
import { executeTool, registerToolPlugin, toolIdentity } from "./lib/tool"
|
||||||
|
|
||||||
|
const globToolNode = makeLocationNode({
|
||||||
|
name: "test/glob-tool-plugin",
|
||||||
|
layer: Layer.effectDiscard(registerToolPlugin(GlobTool.Plugin)),
|
||||||
|
deps: [ToolRegistry.toolsNode, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node],
|
||||||
|
})
|
||||||
|
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],
|
||||||
|
})
|
||||||
|
const permission = Layer.succeed(
|
||||||
|
PermissionV2.Service,
|
||||||
|
PermissionV2.Service.of({
|
||||||
|
assert: () => Effect.void,
|
||||||
|
ask: () => Effect.die("unused"),
|
||||||
|
reply: () => Effect.die("unused"),
|
||||||
|
get: () => Effect.die("unused"),
|
||||||
|
forSession: () => Effect.die("unused"),
|
||||||
|
list: () => Effect.die("unused"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const sessionID = SessionV2.ID.make("ses_search_tool_test")
|
||||||
|
|
||||||
|
const withTools = <A, E, R>(directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect<A, E, R>) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
return yield* body(yield* ToolRegistry.Service)
|
||||||
|
}).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, globToolNode, grepToolNode]), [
|
||||||
|
[
|
||||||
|
Location.node,
|
||||||
|
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
|
||||||
|
],
|
||||||
|
[PermissionV2.node, permission],
|
||||||
|
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const call = (name: "glob" | "grep", input: unknown) => ({
|
||||||
|
sessionID,
|
||||||
|
...toolIdentity,
|
||||||
|
call: { type: "tool-call" as const, id: `call-${name}`, name, input },
|
||||||
|
})
|
||||||
|
|
||||||
|
const it = testEffect(Layer.empty)
|
||||||
|
|
||||||
|
describe("search tools", () => {
|
||||||
|
for (const name of ["glob", "grep"] as const) {
|
||||||
|
it.live(`${name} reports a missing search path`, () =>
|
||||||
|
Effect.acquireUseRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) =>
|
||||||
|
withTools(tmp.path, (registry) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const result = yield* executeTool(
|
||||||
|
registry,
|
||||||
|
call(name, { path: "missing", pattern: name === "glob" ? "*" : "needle" }),
|
||||||
|
)
|
||||||
|
expect(result).toEqual({ type: "error", value: "Search path does not exist: missing" })
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user