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 = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => 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]()), ), ) } })