tool-search.test.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Layer } from "effect"
  3. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  4. import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
  5. import { LayerNode } from "@opencode-ai/core/effect/layer-node"
  6. import { FSUtil } from "@opencode-ai/core/fs-util"
  7. import { Location } from "@opencode-ai/core/location"
  8. import { PermissionV2 } from "@opencode-ai/core/permission"
  9. import { Ripgrep } from "@opencode-ai/core/ripgrep"
  10. import { AbsolutePath } from "@opencode-ai/core/schema"
  11. import { SessionV2 } from "@opencode-ai/core/session"
  12. import { GlobTool } from "@opencode-ai/core/tool/glob"
  13. import { GrepTool } from "@opencode-ai/core/tool/grep"
  14. import { ToolRegistry } from "@opencode-ai/core/tool/registry"
  15. import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
  16. import { location } from "./fixture/location"
  17. import { tmpdir } from "./fixture/tmpdir"
  18. import { testEffect } from "./lib/effect"
  19. import { executeTool, registerToolPlugin, toolIdentity } from "./lib/tool"
  20. const globToolNode = makeLocationNode({
  21. name: "test/glob-tool-plugin",
  22. layer: Layer.effectDiscard(registerToolPlugin(GlobTool.Plugin)),
  23. deps: [ToolRegistry.toolsNode, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node],
  24. })
  25. const grepToolNode = makeLocationNode({
  26. name: "test/grep-tool-plugin",
  27. layer: Layer.effectDiscard(registerToolPlugin(GrepTool.Plugin)),
  28. deps: [ToolRegistry.toolsNode, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node],
  29. })
  30. const permission = Layer.succeed(
  31. PermissionV2.Service,
  32. PermissionV2.Service.of({
  33. assert: () => Effect.void,
  34. ask: () => Effect.die("unused"),
  35. reply: () => Effect.die("unused"),
  36. get: () => Effect.die("unused"),
  37. forSession: () => Effect.die("unused"),
  38. list: () => Effect.die("unused"),
  39. }),
  40. )
  41. const sessionID = SessionV2.ID.make("ses_search_tool_test")
  42. const withTools = <A, E, R>(directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect<A, E, R>) =>
  43. Effect.gen(function* () {
  44. return yield* body(yield* ToolRegistry.Service)
  45. }).pipe(
  46. Effect.provide(
  47. AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, globToolNode, grepToolNode]), [
  48. [
  49. Location.node,
  50. Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
  51. ],
  52. [PermissionV2.node, permission],
  53. [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
  54. ]),
  55. ),
  56. )
  57. const call = (name: "glob" | "grep", input: unknown) => ({
  58. sessionID,
  59. ...toolIdentity,
  60. call: { type: "tool-call" as const, id: `call-${name}`, name, input },
  61. })
  62. const it = testEffect(Layer.empty)
  63. describe("search tools", () => {
  64. for (const name of ["glob", "grep"] as const) {
  65. it.live(`${name} reports a missing search path`, () =>
  66. Effect.acquireUseRelease(
  67. Effect.promise(() => tmpdir()),
  68. (tmp) =>
  69. withTools(tmp.path, (registry) =>
  70. Effect.gen(function* () {
  71. const result = yield* executeTool(
  72. registry,
  73. call(name, { path: "missing", pattern: name === "glob" ? "*" : "needle" }),
  74. )
  75. expect(result).toEqual({ type: "error", value: "Search path does not exist: missing" })
  76. }),
  77. ),
  78. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  79. ),
  80. )
  81. }
  82. })