skill.test.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
  2. import { Effect, Layer } from "effect"
  3. import { afterEach, describe, expect } from "bun:test"
  4. import path from "path"
  5. import { pathToFileURL } from "url"
  6. import type { Permission } from "../../src/permission"
  7. import type { Tool } from "../../src/tool/tool"
  8. import { Instance } from "../../src/project/instance"
  9. import { SkillTool } from "../../src/tool/skill"
  10. import { ToolRegistry } from "../../src/tool/registry"
  11. import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
  12. import { SessionID, MessageID } from "../../src/session/schema"
  13. import { testEffect } from "../lib/effect"
  14. const baseCtx: Omit<Tool.Context, "ask"> = {
  15. sessionID: SessionID.make("ses_test"),
  16. messageID: MessageID.make(""),
  17. callID: "",
  18. agent: "build",
  19. abort: AbortSignal.any([]),
  20. messages: [],
  21. metadata: () => Effect.void,
  22. }
  23. afterEach(async () => {
  24. await Instance.disposeAll()
  25. })
  26. const node = CrossSpawnSpawner.defaultLayer
  27. const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
  28. describe("tool.skill", () => {
  29. it.live("description lists skill location URL", () =>
  30. provideTmpdirInstance(
  31. (dir) =>
  32. Effect.gen(function* () {
  33. const skill = path.join(dir, ".opencode", "skill", "tool-skill")
  34. yield* Effect.promise(() =>
  35. Bun.write(
  36. path.join(skill, "SKILL.md"),
  37. `---
  38. name: tool-skill
  39. description: Skill for tool tests.
  40. ---
  41. # Tool Skill
  42. `,
  43. ),
  44. )
  45. const home = process.env.KILO_TEST_HOME
  46. process.env.KILO_TEST_HOME = dir
  47. yield* Effect.addFinalizer(() =>
  48. Effect.sync(() => {
  49. process.env.KILO_TEST_HOME = home
  50. }),
  51. )
  52. const registry = yield* ToolRegistry.Service
  53. const desc =
  54. (yield* registry.tools({
  55. providerID: "opencode" as any,
  56. modelID: "gpt-5" as any,
  57. agent: { name: "build", mode: "primary", permission: [], options: {} },
  58. })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
  59. expect(desc).toContain("**tool-skill**: Skill for tool tests.")
  60. }),
  61. { git: true },
  62. ),
  63. )
  64. it.live("description sorts skills by name and is stable across calls", () =>
  65. provideTmpdirInstance(
  66. (dir) =>
  67. Effect.gen(function* () {
  68. for (const [name, description] of [
  69. ["zeta-skill", "Zeta skill."],
  70. ["alpha-skill", "Alpha skill."],
  71. ["middle-skill", "Middle skill."],
  72. ]) {
  73. const skill = path.join(dir, ".opencode", "skill", name)
  74. yield* Effect.promise(() =>
  75. Bun.write(
  76. path.join(skill, "SKILL.md"),
  77. `---
  78. name: ${name}
  79. description: ${description}
  80. ---
  81. # ${name}
  82. `,
  83. ),
  84. )
  85. }
  86. const home = process.env.KILO_TEST_HOME
  87. process.env.KILO_TEST_HOME = dir
  88. yield* Effect.addFinalizer(() =>
  89. Effect.sync(() => {
  90. process.env.KILO_TEST_HOME = home
  91. }),
  92. )
  93. const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
  94. const registry = yield* ToolRegistry.Service
  95. const load = Effect.fnUntraced(function* () {
  96. return (
  97. (yield* registry.tools({
  98. providerID: "opencode" as any,
  99. modelID: "gpt-5" as any,
  100. agent,
  101. })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
  102. )
  103. })
  104. const first = yield* load()
  105. const second = yield* load()
  106. expect(first).toBe(second)
  107. const alpha = first.indexOf("**alpha-skill**: Alpha skill.")
  108. const middle = first.indexOf("**middle-skill**: Middle skill.")
  109. const zeta = first.indexOf("**zeta-skill**: Zeta skill.")
  110. expect(alpha).toBeGreaterThan(-1)
  111. expect(middle).toBeGreaterThan(alpha)
  112. expect(zeta).toBeGreaterThan(middle)
  113. }),
  114. { git: true },
  115. ),
  116. )
  117. it.live("execute returns skill content block with files", () =>
  118. provideTmpdirInstance(
  119. (dir) =>
  120. Effect.gen(function* () {
  121. const skill = path.join(dir, ".opencode", "skill", "tool-skill")
  122. yield* Effect.promise(() =>
  123. Bun.write(
  124. path.join(skill, "SKILL.md"),
  125. `---
  126. name: tool-skill
  127. description: Skill for tool tests.
  128. ---
  129. # Tool Skill
  130. Use this skill.
  131. `,
  132. ),
  133. )
  134. yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
  135. const home = process.env.KILO_TEST_HOME
  136. process.env.KILO_TEST_HOME = dir
  137. yield* Effect.addFinalizer(() =>
  138. Effect.sync(() => {
  139. process.env.KILO_TEST_HOME = home
  140. }),
  141. )
  142. const registry = yield* ToolRegistry.Service
  143. const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
  144. const tool = (yield* registry.tools({
  145. providerID: "opencode" as any,
  146. modelID: "gpt-5" as any,
  147. agent,
  148. })).find((tool) => tool.id === SkillTool.id)
  149. if (!tool) throw new Error("Skill tool not found")
  150. const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
  151. const ctx: Tool.Context = {
  152. ...baseCtx,
  153. ask: (req) =>
  154. Effect.sync(() => {
  155. requests.push(req)
  156. }),
  157. }
  158. const result = yield* tool.execute({ name: "tool-skill" }, ctx)
  159. const file = path.resolve(skill, "scripts", "demo.txt")
  160. expect(requests.length).toBe(1)
  161. expect(requests[0].permission).toBe("skill")
  162. expect(requests[0].patterns).toContain("tool-skill")
  163. expect(requests[0].always).toContain("tool-skill")
  164. expect(result.metadata.dir).toBe(skill)
  165. expect(result.output).toContain(`<skill_content name="tool-skill">`)
  166. expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
  167. expect(result.output).toContain(`<file>${file}</file>`)
  168. }),
  169. { git: true },
  170. ),
  171. )
  172. // kilocode_change start
  173. it.live("built-in kilo-config includes named command lookup guidance", () =>
  174. provideTmpdirInstance(
  175. (dir) =>
  176. Effect.gen(function* () {
  177. const home = process.env.KILO_TEST_HOME
  178. process.env.KILO_TEST_HOME = dir
  179. yield* Effect.addFinalizer(() =>
  180. Effect.sync(() => {
  181. process.env.KILO_TEST_HOME = home
  182. }),
  183. )
  184. const registry = yield* ToolRegistry.Service
  185. const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
  186. const tool = (yield* registry.tools({
  187. providerID: "opencode" as any,
  188. modelID: "gpt-5" as any,
  189. agent,
  190. })).find((t) => t.id === SkillTool.id)
  191. if (!tool) throw new Error("Skill tool not found")
  192. const ctx: Tool.Context = {
  193. ...baseCtx,
  194. ask: () => Effect.void,
  195. }
  196. const result = yield* tool.execute({ name: "kilo-config" }, ctx)
  197. expect(result.metadata.dir).toBe("builtin")
  198. expect(result.output).toContain("Finding a named command")
  199. expect(result.output).toContain("~/.config/kilo/")
  200. expect(result.output).toContain("~/.kilocode/")
  201. expect(result.output).toContain("**/command/")
  202. expect(result.output).toContain("explicit search")
  203. }),
  204. { git: true },
  205. ),
  206. )
  207. // kilocode_change end
  208. })