skill.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  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. yield* skill.transform((editor) => {
  59. editor.source({ type: "directory", path: AbsolutePath.make(first) })
  60. editor.source({ type: "directory", path: AbsolutePath.make(first) })
  61. editor.source({ type: "directory", path: AbsolutePath.make(second) })
  62. expect(editor.list()).toEqual([
  63. { type: "directory", path: AbsolutePath.make(first) },
  64. { type: "directory", path: AbsolutePath.make(second) },
  65. ])
  66. })
  67. expect(yield* skill.sources()).toEqual([
  68. { type: "directory", path: AbsolutePath.make(first) },
  69. { type: "directory", path: AbsolutePath.make(second) },
  70. ])
  71. expect(yield* skill.list()).toEqual([
  72. SkillV2.Info.make({
  73. name: "foo",
  74. slash: true,
  75. location: AbsolutePath.make(path.join(first, "foo.md")),
  76. content: "# foo",
  77. }),
  78. {
  79. name: "review",
  80. description: "Second",
  81. location: AbsolutePath.make(path.join(second, "review", "SKILL.md")),
  82. content: "# review",
  83. },
  84. ])
  85. }),
  86. ),
  87. ),
  88. )
  89. it.live("loads URL sources and filters skills for agents", () =>
  90. Effect.acquireRelease(
  91. Effect.promise(() => tmpdir()),
  92. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  93. ).pipe(
  94. Effect.flatMap((tmp) =>
  95. Effect.gen(function* () {
  96. yield* Effect.promise(async () => {
  97. await fs.mkdir(path.join(tmp.path, "deploy"), { recursive: true })
  98. await write(tmp.path, "deploy", "Deploy production")
  99. })
  100. pulls = 0
  101. urls.set("https://example.test/skills/", [AbsolutePath.make(tmp.path)])
  102. const agents = yield* AgentV2.Service
  103. yield* agents.transform((editor) =>
  104. editor.update(AgentV2.ID.make("reviewer"), (agent) => {
  105. agent.permissions.push({ action: "skill", resource: "deploy", effect: "deny" })
  106. }),
  107. )
  108. const skill = yield* SkillV2.Service
  109. yield* skill.transform((editor) => editor.source({ type: "url", url: "https://example.test/skills/" }))
  110. expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
  111. expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
  112. expect(pulls).toBe(1)
  113. expect(SkillV2.available(yield* skill.list(), (yield* agents.get(AgentV2.ID.make("reviewer")))!)).toEqual([])
  114. }),
  115. ),
  116. ),
  117. )
  118. })