skill.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import { Effect, Layer, ManagedRuntime } from "effect"
  2. import { Skill } from "../../src/skill"
  3. import { Ripgrep } from "../../src/file/ripgrep"
  4. import { afterEach, describe, expect, test } from "bun:test"
  5. import path from "path"
  6. import { pathToFileURL } from "url"
  7. import type { Permission } from "../../src/permission"
  8. import type { Tool } from "../../src/tool/tool"
  9. import { Instance } from "../../src/project/instance"
  10. import { SkillTool } from "../../src/tool/skill"
  11. import { ToolRegistry } from "../../src/tool/registry"
  12. import { tmpdir } from "../fixture/fixture"
  13. import { SessionID, MessageID } from "../../src/session/schema"
  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: () => {},
  22. }
  23. afterEach(async () => {
  24. await Instance.disposeAll()
  25. })
  26. describe("tool.skill", () => {
  27. test("description lists skill location URL", async () => {
  28. await using tmp = await tmpdir({
  29. git: true,
  30. init: async (dir) => {
  31. const skillDir = path.join(dir, ".opencode", "skill", "tool-skill")
  32. await Bun.write(
  33. path.join(skillDir, "SKILL.md"),
  34. `---
  35. name: tool-skill
  36. description: Skill for tool tests.
  37. ---
  38. # Tool Skill
  39. `,
  40. )
  41. },
  42. })
  43. const home = process.env.OPENCODE_TEST_HOME
  44. process.env.OPENCODE_TEST_HOME = tmp.path
  45. try {
  46. await Instance.provide({
  47. directory: tmp.path,
  48. fn: async () => {
  49. const desc = await ToolRegistry.tools({
  50. providerID: "opencode" as any,
  51. modelID: "gpt-5" as any,
  52. agent: { name: "build", mode: "primary" as const, permission: [], options: {} },
  53. }).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "")
  54. expect(desc).toContain(`**tool-skill**: Skill for tool tests.`)
  55. },
  56. })
  57. } finally {
  58. process.env.OPENCODE_TEST_HOME = home
  59. }
  60. })
  61. test("description sorts skills by name and is stable across calls", async () => {
  62. await using tmp = await tmpdir({
  63. git: true,
  64. init: async (dir) => {
  65. for (const [name, description] of [
  66. ["zeta-skill", "Zeta skill."],
  67. ["alpha-skill", "Alpha skill."],
  68. ["middle-skill", "Middle skill."],
  69. ]) {
  70. const skillDir = path.join(dir, ".opencode", "skill", name)
  71. await Bun.write(
  72. path.join(skillDir, "SKILL.md"),
  73. `---
  74. name: ${name}
  75. description: ${description}
  76. ---
  77. # ${name}
  78. `,
  79. )
  80. }
  81. },
  82. })
  83. const home = process.env.OPENCODE_TEST_HOME
  84. process.env.OPENCODE_TEST_HOME = tmp.path
  85. try {
  86. await Instance.provide({
  87. directory: tmp.path,
  88. fn: async () => {
  89. const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
  90. const load = () =>
  91. ToolRegistry.tools({
  92. providerID: "opencode" as any,
  93. modelID: "gpt-5" as any,
  94. agent,
  95. }).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "")
  96. const first = await load()
  97. const second = await load()
  98. expect(first).toBe(second)
  99. const alpha = first.indexOf("**alpha-skill**: Alpha skill.")
  100. const middle = first.indexOf("**middle-skill**: Middle skill.")
  101. const zeta = first.indexOf("**zeta-skill**: Zeta skill.")
  102. expect(alpha).toBeGreaterThan(-1)
  103. expect(middle).toBeGreaterThan(alpha)
  104. expect(zeta).toBeGreaterThan(middle)
  105. },
  106. })
  107. } finally {
  108. process.env.OPENCODE_TEST_HOME = home
  109. }
  110. })
  111. test("execute returns skill content block with files", async () => {
  112. await using tmp = await tmpdir({
  113. git: true,
  114. init: async (dir) => {
  115. const skillDir = path.join(dir, ".opencode", "skill", "tool-skill")
  116. await Bun.write(
  117. path.join(skillDir, "SKILL.md"),
  118. `---
  119. name: tool-skill
  120. description: Skill for tool tests.
  121. ---
  122. # Tool Skill
  123. Use this skill.
  124. `,
  125. )
  126. await Bun.write(path.join(skillDir, "scripts", "demo.txt"), "demo")
  127. },
  128. })
  129. const home = process.env.OPENCODE_TEST_HOME
  130. process.env.OPENCODE_TEST_HOME = tmp.path
  131. try {
  132. await Instance.provide({
  133. directory: tmp.path,
  134. fn: async () => {
  135. const runtime = ManagedRuntime.make(Layer.mergeAll(Skill.defaultLayer, Ripgrep.defaultLayer))
  136. const info = await runtime.runPromise(SkillTool)
  137. const tool = await info.init()
  138. const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
  139. const ctx: Tool.Context = {
  140. ...baseCtx,
  141. ask: async (req) => {
  142. requests.push(req)
  143. },
  144. }
  145. const result = await tool.execute({ name: "tool-skill" }, ctx)
  146. const dir = path.join(tmp.path, ".opencode", "skill", "tool-skill")
  147. const file = path.resolve(dir, "scripts", "demo.txt")
  148. expect(requests.length).toBe(1)
  149. expect(requests[0].permission).toBe("skill")
  150. expect(requests[0].patterns).toContain("tool-skill")
  151. expect(requests[0].always).toContain("tool-skill")
  152. expect(result.metadata.dir).toBe(dir)
  153. expect(result.output).toContain(`<skill_content name="tool-skill">`)
  154. expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(dir).href}`)
  155. expect(result.output).toContain(`<file>${file}</file>`)
  156. },
  157. })
  158. } finally {
  159. process.env.OPENCODE_TEST_HOME = home
  160. }
  161. })
  162. })