skill.test.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import { test, expect } from "bun:test"
  2. import { Skill } from "../../src/skill"
  3. import { Instance } from "../../src/project/instance"
  4. import { tmpdir } from "../fixture/fixture"
  5. import path from "path"
  6. import fs from "fs/promises"
  7. async function createGlobalSkill(homeDir: string) {
  8. const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill")
  9. await fs.mkdir(skillDir, { recursive: true })
  10. await Bun.write(
  11. path.join(skillDir, "SKILL.md"),
  12. `---
  13. name: global-test-skill
  14. description: A global skill from ~/.claude/skills for testing.
  15. ---
  16. # Global Test Skill
  17. This skill is loaded from the global home directory.
  18. `,
  19. )
  20. }
  21. test("discovers skills from .opencode/skill/ directory", async () => {
  22. await using tmp = await tmpdir({
  23. git: true,
  24. init: async (dir) => {
  25. const skillDir = path.join(dir, ".opencode", "skill", "test-skill")
  26. await Bun.write(
  27. path.join(skillDir, "SKILL.md"),
  28. `---
  29. name: test-skill
  30. description: A test skill for verification.
  31. ---
  32. # Test Skill
  33. Instructions here.
  34. `,
  35. )
  36. },
  37. })
  38. await Instance.provide({
  39. directory: tmp.path,
  40. fn: async () => {
  41. const skills = await Skill.all()
  42. expect(skills.length).toBe(1)
  43. const testSkill = skills.find((s) => s.name === "test-skill")
  44. expect(testSkill).toBeDefined()
  45. expect(testSkill!.description).toBe("A test skill for verification.")
  46. expect(testSkill!.location).toContain("skill/test-skill/SKILL.md")
  47. },
  48. })
  49. })
  50. test("returns skill directories from Skill.dirs", async () => {
  51. await using tmp = await tmpdir({
  52. git: true,
  53. init: async (dir) => {
  54. const skillDir = path.join(dir, ".opencode", "skill", "dir-skill")
  55. await Bun.write(
  56. path.join(skillDir, "SKILL.md"),
  57. `---
  58. name: dir-skill
  59. description: Skill for dirs test.
  60. ---
  61. # Dir Skill
  62. `,
  63. )
  64. },
  65. })
  66. const home = process.env.OPENCODE_TEST_HOME
  67. process.env.OPENCODE_TEST_HOME = tmp.path
  68. try {
  69. await Instance.provide({
  70. directory: tmp.path,
  71. fn: async () => {
  72. const dirs = await Skill.dirs()
  73. const skillDir = path.join(tmp.path, ".opencode", "skill", "dir-skill")
  74. expect(dirs).toContain(skillDir)
  75. expect(dirs.length).toBe(1)
  76. },
  77. })
  78. } finally {
  79. process.env.OPENCODE_TEST_HOME = home
  80. }
  81. })
  82. test("discovers multiple skills from .opencode/skill/ directory", async () => {
  83. await using tmp = await tmpdir({
  84. git: true,
  85. init: async (dir) => {
  86. const skillDir1 = path.join(dir, ".opencode", "skill", "skill-one")
  87. const skillDir2 = path.join(dir, ".opencode", "skill", "skill-two")
  88. await Bun.write(
  89. path.join(skillDir1, "SKILL.md"),
  90. `---
  91. name: skill-one
  92. description: First test skill.
  93. ---
  94. # Skill One
  95. `,
  96. )
  97. await Bun.write(
  98. path.join(skillDir2, "SKILL.md"),
  99. `---
  100. name: skill-two
  101. description: Second test skill.
  102. ---
  103. # Skill Two
  104. `,
  105. )
  106. },
  107. })
  108. await Instance.provide({
  109. directory: tmp.path,
  110. fn: async () => {
  111. const skills = await Skill.all()
  112. expect(skills.length).toBe(2)
  113. expect(skills.find((s) => s.name === "skill-one")).toBeDefined()
  114. expect(skills.find((s) => s.name === "skill-two")).toBeDefined()
  115. },
  116. })
  117. })
  118. test("skips skills with missing frontmatter", async () => {
  119. await using tmp = await tmpdir({
  120. git: true,
  121. init: async (dir) => {
  122. const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter")
  123. await Bun.write(
  124. path.join(skillDir, "SKILL.md"),
  125. `# No Frontmatter
  126. Just some content without YAML frontmatter.
  127. `,
  128. )
  129. },
  130. })
  131. await Instance.provide({
  132. directory: tmp.path,
  133. fn: async () => {
  134. const skills = await Skill.all()
  135. expect(skills).toEqual([])
  136. },
  137. })
  138. })
  139. test("discovers skills from .claude/skills/ directory", async () => {
  140. await using tmp = await tmpdir({
  141. git: true,
  142. init: async (dir) => {
  143. const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
  144. await Bun.write(
  145. path.join(skillDir, "SKILL.md"),
  146. `---
  147. name: claude-skill
  148. description: A skill in the .claude/skills directory.
  149. ---
  150. # Claude Skill
  151. `,
  152. )
  153. },
  154. })
  155. await Instance.provide({
  156. directory: tmp.path,
  157. fn: async () => {
  158. const skills = await Skill.all()
  159. expect(skills.length).toBe(1)
  160. const claudeSkill = skills.find((s) => s.name === "claude-skill")
  161. expect(claudeSkill).toBeDefined()
  162. expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md")
  163. },
  164. })
  165. })
  166. test("discovers global skills from ~/.claude/skills/ directory", async () => {
  167. await using tmp = await tmpdir({ git: true })
  168. const originalHome = process.env.OPENCODE_TEST_HOME
  169. process.env.OPENCODE_TEST_HOME = tmp.path
  170. try {
  171. await createGlobalSkill(tmp.path)
  172. await Instance.provide({
  173. directory: tmp.path,
  174. fn: async () => {
  175. const skills = await Skill.all()
  176. expect(skills.length).toBe(1)
  177. expect(skills[0].name).toBe("global-test-skill")
  178. expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
  179. expect(skills[0].location).toContain(".claude/skills/global-test-skill/SKILL.md")
  180. },
  181. })
  182. } finally {
  183. process.env.OPENCODE_TEST_HOME = originalHome
  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("discovers skills from .agents/skills/ directory", async () => {
  197. await using tmp = await tmpdir({
  198. git: true,
  199. init: async (dir) => {
  200. const skillDir = path.join(dir, ".agents", "skills", "agent-skill")
  201. await Bun.write(
  202. path.join(skillDir, "SKILL.md"),
  203. `---
  204. name: agent-skill
  205. description: A skill in the .agents/skills directory.
  206. ---
  207. # Agent Skill
  208. `,
  209. )
  210. },
  211. })
  212. await Instance.provide({
  213. directory: tmp.path,
  214. fn: async () => {
  215. const skills = await Skill.all()
  216. expect(skills.length).toBe(1)
  217. const agentSkill = skills.find((s) => s.name === "agent-skill")
  218. expect(agentSkill).toBeDefined()
  219. expect(agentSkill!.location).toContain(".agents/skills/agent-skill/SKILL.md")
  220. },
  221. })
  222. })
  223. test("discovers global skills from ~/.agents/skills/ directory", async () => {
  224. await using tmp = await tmpdir({ git: true })
  225. const originalHome = process.env.OPENCODE_TEST_HOME
  226. process.env.OPENCODE_TEST_HOME = tmp.path
  227. try {
  228. const skillDir = path.join(tmp.path, ".agents", "skills", "global-agent-skill")
  229. await fs.mkdir(skillDir, { recursive: true })
  230. await Bun.write(
  231. path.join(skillDir, "SKILL.md"),
  232. `---
  233. name: global-agent-skill
  234. description: A global skill from ~/.agents/skills for testing.
  235. ---
  236. # Global Agent Skill
  237. This skill is loaded from the global home directory.
  238. `,
  239. )
  240. await Instance.provide({
  241. directory: tmp.path,
  242. fn: async () => {
  243. const skills = await Skill.all()
  244. expect(skills.length).toBe(1)
  245. expect(skills[0].name).toBe("global-agent-skill")
  246. expect(skills[0].description).toBe("A global skill from ~/.agents/skills for testing.")
  247. expect(skills[0].location).toContain(".agents/skills/global-agent-skill/SKILL.md")
  248. },
  249. })
  250. } finally {
  251. process.env.OPENCODE_TEST_HOME = originalHome
  252. }
  253. })
  254. test("discovers skills from both .claude/skills/ and .agents/skills/", async () => {
  255. await using tmp = await tmpdir({
  256. git: true,
  257. init: async (dir) => {
  258. const claudeDir = path.join(dir, ".claude", "skills", "claude-skill")
  259. const agentDir = path.join(dir, ".agents", "skills", "agent-skill")
  260. await Bun.write(
  261. path.join(claudeDir, "SKILL.md"),
  262. `---
  263. name: claude-skill
  264. description: A skill in the .claude/skills directory.
  265. ---
  266. # Claude Skill
  267. `,
  268. )
  269. await Bun.write(
  270. path.join(agentDir, "SKILL.md"),
  271. `---
  272. name: agent-skill
  273. description: A skill in the .agents/skills directory.
  274. ---
  275. # Agent Skill
  276. `,
  277. )
  278. },
  279. })
  280. await Instance.provide({
  281. directory: tmp.path,
  282. fn: async () => {
  283. const skills = await Skill.all()
  284. expect(skills.length).toBe(2)
  285. expect(skills.find((s) => s.name === "claude-skill")).toBeDefined()
  286. expect(skills.find((s) => s.name === "agent-skill")).toBeDefined()
  287. },
  288. })
  289. })