| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- import { describe, expect } from "bun:test"
- import fs from "fs/promises"
- import path from "path"
- import { Effect, Layer } from "effect"
- import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
- import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
- import { LayerNode } from "@opencode-ai/util/effect/layer-node"
- import { FileSystem } from "@opencode-ai/core/filesystem"
- import { FSUtil } from "@opencode-ai/util/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, settleTool, 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", () => {
- it.live("bounds omitted glob and grep limits", () =>
- Effect.acquireUseRelease(
- Effect.promise(() => tmpdir()),
- (tmp) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Promise.all(
- Array.from({ length: FileSystem.DEFAULT_SEARCH_LIMIT + 1 }, (_, index) =>
- fs.writeFile(path.join(tmp.path, `${index}.txt`), "needle\n"),
- ),
- ),
- )
- yield* withTools(tmp.path, (registry) =>
- Effect.gen(function* () {
- const glob = yield* settleTool(registry, call("glob", { pattern: "*" }))
- const grep = yield* settleTool(registry, call("grep", { pattern: "needle" }))
- expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT })
- expect(grep.output?.structured).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT })
- expect(glob.output?.content).toEqual([{ type: "text", text: String(glob.result.value) }])
- expect(grep.output?.content).toEqual([{ type: "text", text: String(grep.result.value) }])
- expect(String(glob.result.value).split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT)
- expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`)
- }),
- )
- }),
- (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
- ),
- )
- 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]()),
- ),
- )
- }
- })
|