|
|
@@ -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]()),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ }
|
|
|
+})
|