skill.test.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import { test, expect } from "bun:test"
  2. import { Skill } from "../../src/skill"
  3. import { SystemPrompt } from "../../src/session/system"
  4. import { Instance } from "../../src/project/instance"
  5. import { tmpdir } from "../fixture/fixture"
  6. import path from "path"
  7. test("discovers skills from .opencode/skill/ directory", async () => {
  8. await using tmp = await tmpdir({
  9. git: true,
  10. init: async (dir) => {
  11. const skillDir = path.join(dir, ".opencode", "skill", "test-skill")
  12. await Bun.write(
  13. path.join(skillDir, "SKILL.md"),
  14. `---
  15. name: test-skill
  16. description: A test skill for verification.
  17. ---
  18. # Test Skill
  19. Instructions here.
  20. `,
  21. )
  22. },
  23. })
  24. await Instance.provide({
  25. directory: tmp.path,
  26. fn: async () => {
  27. const skills = await Skill.all()
  28. expect(skills.length).toBe(1)
  29. expect(skills[0].name).toBe("test-skill")
  30. expect(skills[0].description).toBe("A test skill for verification.")
  31. expect(skills[0].location).toContain("skill/test-skill/SKILL.md")
  32. },
  33. })
  34. })
  35. test("discovers multiple skills from .opencode/skill/ directory", async () => {
  36. await using tmp = await tmpdir({
  37. git: true,
  38. init: async (dir) => {
  39. const skillDir = path.join(dir, ".opencode", "skill", "my-skill")
  40. await Bun.write(
  41. path.join(skillDir, "SKILL.md"),
  42. `---
  43. name: my-skill
  44. description: Another test skill.
  45. ---
  46. # My Skill
  47. `,
  48. )
  49. },
  50. })
  51. await Instance.provide({
  52. directory: tmp.path,
  53. fn: async () => {
  54. const skills = await Skill.all()
  55. expect(skills.length).toBe(1)
  56. expect(skills[0].name).toBe("my-skill")
  57. },
  58. })
  59. })
  60. test("throws error for invalid skill name format", async () => {
  61. await using tmp = await tmpdir({
  62. git: true,
  63. init: async (dir) => {
  64. const skillDir = path.join(dir, ".opencode", "skill", "InvalidName")
  65. await Bun.write(
  66. path.join(skillDir, "SKILL.md"),
  67. `---
  68. name: InvalidName
  69. description: A skill with invalid name.
  70. ---
  71. `,
  72. )
  73. },
  74. })
  75. await Instance.provide({
  76. directory: tmp.path,
  77. fn: async () => {
  78. await expect(Skill.all()).rejects.toThrow()
  79. },
  80. })
  81. })
  82. test("throws error when name doesn't match directory", async () => {
  83. await using tmp = await tmpdir({
  84. git: true,
  85. init: async (dir) => {
  86. const skillDir = path.join(dir, ".opencode", "skill", "dir-name")
  87. await Bun.write(
  88. path.join(skillDir, "SKILL.md"),
  89. `---
  90. name: different-name
  91. description: A skill with mismatched name.
  92. ---
  93. `,
  94. )
  95. },
  96. })
  97. await Instance.provide({
  98. directory: tmp.path,
  99. fn: async () => {
  100. await expect(Skill.all()).rejects.toThrow("SkillNameMismatchError")
  101. },
  102. })
  103. })
  104. test("throws error for missing frontmatter", async () => {
  105. await using tmp = await tmpdir({
  106. git: true,
  107. init: async (dir) => {
  108. const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter")
  109. await Bun.write(
  110. path.join(skillDir, "SKILL.md"),
  111. `# No Frontmatter
  112. Just some content without YAML frontmatter.
  113. `,
  114. )
  115. },
  116. })
  117. await Instance.provide({
  118. directory: tmp.path,
  119. fn: async () => {
  120. await expect(Skill.all()).rejects.toThrow("SkillInvalidError")
  121. },
  122. })
  123. })
  124. test("parses optional fields", async () => {
  125. await using tmp = await tmpdir({
  126. git: true,
  127. init: async (dir) => {
  128. const skillDir = path.join(dir, ".opencode", "skill", "full-skill")
  129. await Bun.write(
  130. path.join(skillDir, "SKILL.md"),
  131. `---
  132. name: full-skill
  133. description: A skill with all optional fields.
  134. license: MIT
  135. compatibility: Requires Node.js 18+
  136. metadata:
  137. author: test-author
  138. version: "1.0"
  139. ---
  140. # Full Skill
  141. `,
  142. )
  143. },
  144. })
  145. await Instance.provide({
  146. directory: tmp.path,
  147. fn: async () => {
  148. const skills = await Skill.all()
  149. expect(skills.length).toBe(1)
  150. expect(skills[0].license).toBe("MIT")
  151. expect(skills[0].compatibility).toBe("Requires Node.js 18+")
  152. expect(skills[0].metadata).toEqual({
  153. author: "test-author",
  154. version: "1.0",
  155. })
  156. },
  157. })
  158. })
  159. test("ignores unknown frontmatter fields", async () => {
  160. await using tmp = await tmpdir({
  161. git: true,
  162. init: async (dir) => {
  163. const skillDir = path.join(dir, ".opencode", "skill", "extra-fields")
  164. await Bun.write(
  165. path.join(skillDir, "SKILL.md"),
  166. `---
  167. name: extra-fields
  168. description: A skill with extra unknown fields.
  169. allowed-tools: Bash Read Write
  170. some-other-field: ignored
  171. ---
  172. # Extra Fields Skill
  173. `,
  174. )
  175. },
  176. })
  177. await Instance.provide({
  178. directory: tmp.path,
  179. fn: async () => {
  180. const skills = await Skill.all()
  181. expect(skills.length).toBe(1)
  182. expect(skills[0].name).toBe("extra-fields")
  183. },
  184. })
  185. })
  186. test("returns empty array when no skills exist", async () => {
  187. await using tmp = await tmpdir({ git: true })
  188. await Instance.provide({
  189. directory: tmp.path,
  190. fn: async () => {
  191. const skills = await Skill.all()
  192. expect(skills).toEqual([])
  193. },
  194. })
  195. })
  196. test("SystemPrompt.skills() returns empty array when no skills", async () => {
  197. await using tmp = await tmpdir({ git: true })
  198. await Instance.provide({
  199. directory: tmp.path,
  200. fn: async () => {
  201. const result = await SystemPrompt.skills()
  202. expect(result).toEqual([])
  203. },
  204. })
  205. })
  206. test("SystemPrompt.skills() returns XML block with skills", async () => {
  207. await using tmp = await tmpdir({
  208. git: true,
  209. init: async (dir) => {
  210. const skillDir = path.join(dir, ".opencode", "skill", "example-skill")
  211. await Bun.write(
  212. path.join(skillDir, "SKILL.md"),
  213. `---
  214. name: example-skill
  215. description: An example skill for testing XML output.
  216. ---
  217. # Example
  218. `,
  219. )
  220. },
  221. })
  222. await Instance.provide({
  223. directory: tmp.path,
  224. fn: async () => {
  225. const result = await SystemPrompt.skills()
  226. expect(result.length).toBe(1)
  227. expect(result[0]).toContain("<available_skills>")
  228. expect(result[0]).toContain("<name>example-skill</name>")
  229. expect(result[0]).toContain("<description>An example skill for testing XML output.</description>")
  230. expect(result[0]).toContain("SKILL.md</location>")
  231. expect(result[0]).toContain("</available_skills>")
  232. expect(result[0]).toContain("When a task matches a skill's description")
  233. },
  234. })
  235. })
  236. // test("discovers skills from .claude/skills/ directory", async () => {
  237. // await using tmp = await tmpdir({
  238. // git: true,
  239. // init: async (dir) => {
  240. // const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
  241. // await Bun.write(
  242. // path.join(skillDir, "SKILL.md"),
  243. // `---
  244. // name: claude-skill
  245. // description: A skill in the .claude/skills directory.
  246. // ---
  247. // # Claude Skill
  248. // `,
  249. // )
  250. // },
  251. // })
  252. // await Instance.provide({
  253. // directory: tmp.path,
  254. // fn: async () => {
  255. // const skills = await Skill.all()
  256. // expect(skills.length).toBe(1)
  257. // expect(skills[0].name).toBe("claude-skill")
  258. // expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
  259. // },
  260. // })
  261. // })