skill.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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("discovers multiple skills from .opencode/skill/ directory", async () => {
  51. await using tmp = await tmpdir({
  52. git: true,
  53. init: async (dir) => {
  54. const skillDir1 = path.join(dir, ".opencode", "skill", "skill-one")
  55. const skillDir2 = path.join(dir, ".opencode", "skill", "skill-two")
  56. await Bun.write(
  57. path.join(skillDir1, "SKILL.md"),
  58. `---
  59. name: skill-one
  60. description: First test skill.
  61. ---
  62. # Skill One
  63. `,
  64. )
  65. await Bun.write(
  66. path.join(skillDir2, "SKILL.md"),
  67. `---
  68. name: skill-two
  69. description: Second test skill.
  70. ---
  71. # Skill Two
  72. `,
  73. )
  74. },
  75. })
  76. await Instance.provide({
  77. directory: tmp.path,
  78. fn: async () => {
  79. const skills = await Skill.all()
  80. expect(skills.length).toBe(2)
  81. expect(skills.find((s) => s.name === "skill-one")).toBeDefined()
  82. expect(skills.find((s) => s.name === "skill-two")).toBeDefined()
  83. },
  84. })
  85. })
  86. test("skips skills with missing frontmatter", async () => {
  87. await using tmp = await tmpdir({
  88. git: true,
  89. init: async (dir) => {
  90. const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter")
  91. await Bun.write(
  92. path.join(skillDir, "SKILL.md"),
  93. `# No Frontmatter
  94. Just some content without YAML frontmatter.
  95. `,
  96. )
  97. },
  98. })
  99. await Instance.provide({
  100. directory: tmp.path,
  101. fn: async () => {
  102. const skills = await Skill.all()
  103. expect(skills).toEqual([])
  104. },
  105. })
  106. })
  107. test("discovers skills from .claude/skills/ directory", async () => {
  108. await using tmp = await tmpdir({
  109. git: true,
  110. init: async (dir) => {
  111. const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
  112. await Bun.write(
  113. path.join(skillDir, "SKILL.md"),
  114. `---
  115. name: claude-skill
  116. description: A skill in the .claude/skills directory.
  117. ---
  118. # Claude Skill
  119. `,
  120. )
  121. },
  122. })
  123. await Instance.provide({
  124. directory: tmp.path,
  125. fn: async () => {
  126. const skills = await Skill.all()
  127. expect(skills.length).toBe(1)
  128. const claudeSkill = skills.find((s) => s.name === "claude-skill")
  129. expect(claudeSkill).toBeDefined()
  130. expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md")
  131. },
  132. })
  133. })
  134. test("discovers global skills from ~/.claude/skills/ directory", async () => {
  135. await using tmp = await tmpdir({ git: true })
  136. const originalHome = process.env.OPENCODE_TEST_HOME
  137. process.env.OPENCODE_TEST_HOME = tmp.path
  138. try {
  139. await createGlobalSkill(tmp.path)
  140. await Instance.provide({
  141. directory: tmp.path,
  142. fn: async () => {
  143. const skills = await Skill.all()
  144. expect(skills.length).toBe(1)
  145. expect(skills[0].name).toBe("global-test-skill")
  146. expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
  147. expect(skills[0].location).toContain(".claude/skills/global-test-skill/SKILL.md")
  148. },
  149. })
  150. } finally {
  151. process.env.OPENCODE_TEST_HOME = originalHome
  152. }
  153. })
  154. test("returns empty array when no skills exist", async () => {
  155. await using tmp = await tmpdir({ git: true })
  156. await Instance.provide({
  157. directory: tmp.path,
  158. fn: async () => {
  159. const skills = await Skill.all()
  160. expect(skills).toEqual([])
  161. },
  162. })
  163. })