registry.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { afterEach, describe, expect } from "bun:test"
  2. import path from "path"
  3. import fs from "fs/promises"
  4. import { Effect, Layer } from "effect"
  5. import { Instance } from "../../src/project/instance"
  6. import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
  7. import { ToolRegistry } from "../../src/tool"
  8. import { provideTmpdirInstance } from "../fixture/fixture"
  9. import { testEffect } from "../lib/effect"
  10. const node = CrossSpawnSpawner.defaultLayer
  11. const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
  12. afterEach(async () => {
  13. await Instance.disposeAll()
  14. })
  15. describe("tool.registry", () => {
  16. it.live("loads tools from .opencode/tool (singular)", () =>
  17. provideTmpdirInstance((dir) =>
  18. Effect.gen(function* () {
  19. const opencode = path.join(dir, ".opencode")
  20. const tool = path.join(opencode, "tool")
  21. yield* Effect.promise(() => fs.mkdir(tool, { recursive: true }))
  22. yield* Effect.promise(() =>
  23. Bun.write(
  24. path.join(tool, "hello.ts"),
  25. [
  26. "export default {",
  27. " description: 'hello tool',",
  28. " args: {},",
  29. " execute: async () => {",
  30. " return 'hello world'",
  31. " },",
  32. "}",
  33. "",
  34. ].join("\n"),
  35. ),
  36. )
  37. const registry = yield* ToolRegistry.Service
  38. const ids = yield* registry.ids()
  39. expect(ids).toContain("hello")
  40. }),
  41. ),
  42. )
  43. it.live("loads tools from .opencode/tools (plural)", () =>
  44. provideTmpdirInstance((dir) =>
  45. Effect.gen(function* () {
  46. const opencode = path.join(dir, ".opencode")
  47. const tools = path.join(opencode, "tools")
  48. yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
  49. yield* Effect.promise(() =>
  50. Bun.write(
  51. path.join(tools, "hello.ts"),
  52. [
  53. "export default {",
  54. " description: 'hello tool',",
  55. " args: {},",
  56. " execute: async () => {",
  57. " return 'hello world'",
  58. " },",
  59. "}",
  60. "",
  61. ].join("\n"),
  62. ),
  63. )
  64. const registry = yield* ToolRegistry.Service
  65. const ids = yield* registry.ids()
  66. expect(ids).toContain("hello")
  67. }),
  68. ),
  69. )
  70. it.live("loads tools with external dependencies without crashing", () =>
  71. provideTmpdirInstance((dir) =>
  72. Effect.gen(function* () {
  73. const opencode = path.join(dir, ".opencode")
  74. const tools = path.join(opencode, "tools")
  75. yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
  76. yield* Effect.promise(() =>
  77. Bun.write(
  78. path.join(opencode, "package.json"),
  79. JSON.stringify({
  80. name: "custom-tools",
  81. dependencies: {
  82. "@opencode-ai/plugin": "^0.0.0",
  83. cowsay: "^1.6.0",
  84. },
  85. }),
  86. ),
  87. )
  88. yield* Effect.promise(() =>
  89. Bun.write(
  90. path.join(opencode, "package-lock.json"),
  91. JSON.stringify({
  92. name: "custom-tools",
  93. lockfileVersion: 3,
  94. packages: {
  95. "": {
  96. dependencies: {
  97. "@opencode-ai/plugin": "^0.0.0",
  98. cowsay: "^1.6.0",
  99. },
  100. },
  101. },
  102. }),
  103. ),
  104. )
  105. const cowsay = path.join(opencode, "node_modules", "cowsay")
  106. yield* Effect.promise(() => fs.mkdir(cowsay, { recursive: true }))
  107. yield* Effect.promise(() =>
  108. Bun.write(
  109. path.join(cowsay, "package.json"),
  110. JSON.stringify({
  111. name: "cowsay",
  112. type: "module",
  113. exports: "./index.js",
  114. }),
  115. ),
  116. )
  117. yield* Effect.promise(() =>
  118. Bun.write(
  119. path.join(cowsay, "index.js"),
  120. ["export function say({ text }) {", " return `moo ${text}`", "}", ""].join("\n"),
  121. ),
  122. )
  123. yield* Effect.promise(() =>
  124. Bun.write(
  125. path.join(tools, "cowsay.ts"),
  126. [
  127. "import { say } from 'cowsay'",
  128. "export default {",
  129. " description: 'tool that imports cowsay at top level',",
  130. " args: { text: { type: 'string' } },",
  131. " execute: async ({ text }: { text: string }) => {",
  132. " return say({ text })",
  133. " },",
  134. "}",
  135. "",
  136. ].join("\n"),
  137. ),
  138. )
  139. const registry = yield* ToolRegistry.Service
  140. const ids = yield* registry.ids()
  141. expect(ids).toContain("cowsay")
  142. }),
  143. ),
  144. )
  145. })