skill.test.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import fs from "fs/promises"
  2. import path from "path"
  3. import { describe, expect } from "bun:test"
  4. import { Effect, Layer } from "effect"
  5. import { AgentV2 } from "@opencode-ai/core/agent"
  6. import { FSUtil } from "@opencode-ai/core/fs-util"
  7. import { AbsolutePath } from "@opencode-ai/core/schema"
  8. import { SkillV2 } from "@opencode-ai/core/skill"
  9. import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
  10. import { tmpdir } from "./fixture/tmpdir"
  11. import { testEffect } from "./lib/effect"
  12. const urls = new Map<string, AbsolutePath[]>()
  13. let pulls = 0
  14. const discovery = Layer.succeed(
  15. SkillDiscovery.Service,
  16. SkillDiscovery.Service.of({
  17. pull: (url) => {
  18. pulls++
  19. return Effect.succeed(urls.get(url) ?? [])
  20. },
  21. }),
  22. )
  23. const it = testEffect(
  24. SkillV2.layer.pipe(
  25. Layer.provide(discovery),
  26. Layer.provide(FSUtil.defaultLayer),
  27. Layer.provideMerge(AgentV2.locationLayer),
  28. ),
  29. )
  30. function write(directory: string, name: string, description: string) {
  31. return fs.writeFile(
  32. path.join(directory, name, "SKILL.md"),
  33. `---
  34. name: ${name}
  35. description: ${description}
  36. ---
  37. # ${name}`,
  38. )
  39. }
  40. describe("SkillV2", () => {
  41. it.live("registers sources and resolves later source precedence", () =>
  42. Effect.acquireRelease(
  43. Effect.promise(() => tmpdir()),
  44. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  45. ).pipe(
  46. Effect.flatMap((tmp) =>
  47. Effect.gen(function* () {
  48. const first = path.join(tmp.path, "first")
  49. const second = path.join(tmp.path, "second")
  50. yield* Effect.promise(async () => {
  51. await fs.mkdir(path.join(first, "review"), { recursive: true })
  52. await fs.mkdir(path.join(second, "review"), { recursive: true })
  53. await write(first, "review", "First")
  54. await write(second, "review", "Second")
  55. await fs.writeFile(path.join(first, "foo.md"), "---\nslash: true\n---\n# foo")
  56. })
  57. const skill = yield* SkillV2.Service
  58. const register = yield* skill.transform()
  59. yield* register((editor) => {
  60. editor.source({ type: "directory", path: AbsolutePath.make(first) })
  61. editor.source({ type: "directory", path: AbsolutePath.make(first) })
  62. editor.source({ type: "directory", path: AbsolutePath.make(second) })
  63. expect(editor.list()).toEqual([
  64. { type: "directory", path: AbsolutePath.make(first) },
  65. { type: "directory", path: AbsolutePath.make(second) },
  66. ])
  67. })
  68. expect(yield* skill.sources()).toEqual([
  69. { type: "directory", path: AbsolutePath.make(first) },
  70. { type: "directory", path: AbsolutePath.make(second) },
  71. ])
  72. expect(yield* skill.list()).toEqual([
  73. new SkillV2.Info({
  74. name: "foo",
  75. slash: true,
  76. location: AbsolutePath.make(path.join(first, "foo.md")),
  77. content: "# foo",
  78. }),
  79. {
  80. name: "review",
  81. description: "Second",
  82. location: AbsolutePath.make(path.join(second, "review", "SKILL.md")),
  83. content: "# review",
  84. },
  85. ])
  86. }),
  87. ),
  88. ),
  89. )
  90. it.live("loads URL sources and filters skills for agents", () =>
  91. Effect.acquireRelease(
  92. Effect.promise(() => tmpdir()),
  93. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  94. ).pipe(
  95. Effect.flatMap((tmp) =>
  96. Effect.gen(function* () {
  97. yield* Effect.promise(async () => {
  98. await fs.mkdir(path.join(tmp.path, "deploy"), { recursive: true })
  99. await write(tmp.path, "deploy", "Deploy production")
  100. })
  101. pulls = 0
  102. urls.set("https://example.test/skills/", [AbsolutePath.make(tmp.path)])
  103. const agents = yield* AgentV2.Service
  104. yield* agents.update((editor) =>
  105. editor.update(AgentV2.ID.make("reviewer"), (agent) => {
  106. agent.permissions.push({ action: "skill", resource: "deploy", effect: "deny" })
  107. }),
  108. )
  109. const skill = yield* SkillV2.Service
  110. const register = yield* skill.transform()
  111. yield* register((editor) => editor.source({ type: "url", url: "https://example.test/skills/" }))
  112. expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
  113. expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
  114. expect(pulls).toBe(1)
  115. expect(yield* skill.forAgent(AgentV2.ID.make("reviewer"))).toEqual([])
  116. expect(yield* skill.forAgent(AgentV2.ID.make("missing"))).toEqual([])
  117. }),
  118. ),
  119. ),
  120. )
  121. })