skill.test.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { CrossSpawnSpawner } from "@opencode-ai/core/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 "@/tool/tool"
  8. import { Instance } from "../../src/project/instance"
  9. import { SkillTool } from "../../src/tool/skill"
  10. import { ToolRegistry } from "@/tool/registry"
  11. import { disposeAllInstances, provideTmpdirInstance } 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("msg_test"),
  17. callID: "",
  18. agent: "build",
  19. abort: AbortSignal.any([]),
  20. messages: [],
  21. metadata: () => Effect.void,
  22. }
  23. afterEach(async () => {
  24. await disposeAllInstances()
  25. })
  26. const node = CrossSpawnSpawner.defaultLayer
  27. const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
  28. describe("tool.skill", () => {
  29. it.live("execute returns skill content block with files", () =>
  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. Use this skill.
  43. `,
  44. ),
  45. )
  46. yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
  47. const home = process.env.OPENCODE_TEST_HOME
  48. process.env.OPENCODE_TEST_HOME = dir
  49. yield* Effect.addFinalizer(() =>
  50. Effect.sync(() => {
  51. process.env.OPENCODE_TEST_HOME = home
  52. }),
  53. )
  54. const registry = yield* ToolRegistry.Service
  55. const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
  56. const tool = (yield* registry.tools({
  57. providerID: "opencode" as any,
  58. modelID: "gpt-5" as any,
  59. agent,
  60. })).find((tool) => tool.id === SkillTool.id)
  61. if (!tool) throw new Error("Skill tool not found")
  62. const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
  63. const ctx: Tool.Context = {
  64. ...baseCtx,
  65. ask: (req) =>
  66. Effect.sync(() => {
  67. requests.push(req)
  68. }),
  69. }
  70. const result = yield* tool.execute({ name: "tool-skill" }, ctx)
  71. const file = path.resolve(skill, "scripts", "demo.txt")
  72. expect(requests.length).toBe(1)
  73. expect(requests[0].permission).toBe("skill")
  74. expect(requests[0].patterns).toContain("tool-skill")
  75. expect(requests[0].always).toContain("tool-skill")
  76. expect(result.metadata.dir).toBe(skill)
  77. expect(result.output).toContain(`<skill_content name="tool-skill">`)
  78. expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
  79. expect(result.output).toContain(`<file>${file}</file>`)
  80. }),
  81. { git: true },
  82. ),
  83. )
  84. })