registry.test.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { afterEach, describe, expect, test } 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/registry"
  8. import { provideTmpdirInstance, tmpdir } 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. // kilocode_change start - plan_exit is always registered
  17. it.live("plan_exit is always registered regardless of client", () =>
  18. Effect.gen(function* () {
  19. const original = process.env["KILO_CLIENT"]
  20. try {
  21. for (const client of ["cli", "vscode", "desktop", "app"]) {
  22. process.env["KILO_CLIENT"] = client
  23. yield* provideTmpdirInstance(
  24. () =>
  25. Effect.gen(function* () {
  26. const registry = yield* ToolRegistry.Service
  27. const ids = yield* registry.ids()
  28. expect(ids).toContain("plan_exit")
  29. }),
  30. { git: true },
  31. )
  32. }
  33. } finally {
  34. if (original === undefined) delete process.env["KILO_CLIENT"]
  35. else process.env["KILO_CLIENT"] = original
  36. }
  37. }),
  38. )
  39. // kilocode_change end
  40. // kilocode_change start
  41. test("suggest is registered for cli and vscode only", async () => {
  42. const original = process.env["KILO_CLIENT"]
  43. const originalQuestion = process.env["KILO_ENABLE_QUESTION_TOOL"]
  44. const originalConfig = process.env["KILO_CONFIG_DIR"]
  45. try {
  46. for (const client of ["cli", "vscode", "desktop", "app"]) {
  47. process.env["KILO_CLIENT"] = client
  48. process.env["KILO_ENABLE_QUESTION_TOOL"] = client === "vscode" ? "true" : "false"
  49. await using tmp = await tmpdir({ git: true })
  50. process.env["KILO_CONFIG_DIR"] = tmp.path
  51. await Instance.provide({
  52. directory: tmp.path,
  53. fn: async () => {
  54. const ids = await ToolRegistry.ids()
  55. if (client === "cli" || client === "vscode") expect(ids).toContain("suggest")
  56. else expect(ids).not.toContain("suggest")
  57. },
  58. })
  59. }
  60. } finally {
  61. if (original === undefined) delete process.env["KILO_CLIENT"]
  62. else process.env["KILO_CLIENT"] = original
  63. if (originalQuestion === undefined) delete process.env["KILO_ENABLE_QUESTION_TOOL"]
  64. else process.env["KILO_ENABLE_QUESTION_TOOL"] = originalQuestion
  65. if (originalConfig === undefined) delete process.env["KILO_CONFIG_DIR"]
  66. else process.env["KILO_CONFIG_DIR"] = originalConfig
  67. }
  68. })
  69. // kilocode_change end
  70. it.live("loads tools from .opencode/tool (singular)", () =>
  71. provideTmpdirInstance((dir) =>
  72. Effect.gen(function* () {
  73. const opencode = path.join(dir, ".opencode")
  74. const tool = path.join(opencode, "tool")
  75. yield* Effect.promise(() => fs.mkdir(tool, { recursive: true }))
  76. yield* Effect.promise(() =>
  77. Bun.write(
  78. path.join(tool, "hello.ts"),
  79. [
  80. "export default {",
  81. " description: 'hello tool',",
  82. " args: {},",
  83. " execute: async () => {",
  84. " return 'hello world'",
  85. " },",
  86. "}",
  87. "",
  88. ].join("\n"),
  89. ),
  90. )
  91. const registry = yield* ToolRegistry.Service
  92. const ids = yield* registry.ids()
  93. expect(ids).toContain("hello")
  94. }),
  95. ),
  96. )
  97. it.live("loads tools from .opencode/tools (plural)", () =>
  98. provideTmpdirInstance((dir) =>
  99. Effect.gen(function* () {
  100. const opencode = path.join(dir, ".opencode")
  101. const tools = path.join(opencode, "tools")
  102. yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
  103. yield* Effect.promise(() =>
  104. Bun.write(
  105. path.join(tools, "hello.ts"),
  106. [
  107. "export default {",
  108. " description: 'hello tool',",
  109. " args: {},",
  110. " execute: async () => {",
  111. " return 'hello world'",
  112. " },",
  113. "}",
  114. "",
  115. ].join("\n"),
  116. ),
  117. )
  118. const registry = yield* ToolRegistry.Service
  119. const ids = yield* registry.ids()
  120. expect(ids).toContain("hello")
  121. }),
  122. ),
  123. )
  124. it.live("loads tools with external dependencies without crashing", () =>
  125. provideTmpdirInstance((dir) =>
  126. Effect.gen(function* () {
  127. const opencode = path.join(dir, ".opencode")
  128. const tools = path.join(opencode, "tools")
  129. yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
  130. yield* Effect.promise(() =>
  131. Bun.write(
  132. path.join(opencode, "package.json"),
  133. JSON.stringify({
  134. name: "custom-tools",
  135. dependencies: {
  136. "@kilocode/plugin": "^0.0.0",
  137. cowsay: "^1.6.0",
  138. },
  139. }),
  140. ),
  141. )
  142. yield* Effect.promise(() =>
  143. Bun.write(
  144. path.join(opencode, "package-lock.json"),
  145. JSON.stringify({
  146. name: "custom-tools",
  147. lockfileVersion: 3,
  148. packages: {
  149. "": {
  150. dependencies: {
  151. "@kilocode/plugin": "^0.0.0",
  152. cowsay: "^1.6.0",
  153. },
  154. },
  155. },
  156. }),
  157. ),
  158. )
  159. const cowsay = path.join(opencode, "node_modules", "cowsay")
  160. yield* Effect.promise(() => fs.mkdir(cowsay, { recursive: true }))
  161. yield* Effect.promise(() =>
  162. Bun.write(
  163. path.join(cowsay, "package.json"),
  164. JSON.stringify({
  165. name: "cowsay",
  166. type: "module",
  167. exports: "./index.js",
  168. }),
  169. ),
  170. )
  171. yield* Effect.promise(() =>
  172. Bun.write(
  173. path.join(cowsay, "index.js"),
  174. ["export function say({ text }) {", " return `moo ${text}`", "}", ""].join("\n"),
  175. ),
  176. )
  177. yield* Effect.promise(() =>
  178. Bun.write(
  179. path.join(tools, "cowsay.ts"),
  180. [
  181. "import { say } from 'cowsay'",
  182. "export default {",
  183. " description: 'tool that imports cowsay at top level',",
  184. " args: { text: { type: 'string' } },",
  185. " execute: async ({ text }: { text: string }) => {",
  186. " return say({ text })",
  187. " },",
  188. "}",
  189. "",
  190. ].join("\n"),
  191. ),
  192. )
  193. const registry = yield* ToolRegistry.Service
  194. const ids = yield* registry.ids()
  195. expect(ids).toContain("cowsay")
  196. }),
  197. ),
  198. )
  199. })