test-skills-core.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. #!/usr/bin/env bash
  2. # Test: Skills Core Library
  3. # Tests the skills-core.js library functions directly via Node.js
  4. # Does not require OpenCode - tests pure library functionality
  5. set -euo pipefail
  6. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  7. echo "=== Test: Skills Core Library ==="
  8. # Source setup to create isolated environment
  9. source "$SCRIPT_DIR/setup.sh"
  10. # Trap to cleanup on exit
  11. trap cleanup_test_env EXIT
  12. # Test 1: Test extractFrontmatter function
  13. echo "Test 1: Testing extractFrontmatter..."
  14. # Create test file with frontmatter
  15. test_skill_dir="$TEST_HOME/test-skill"
  16. mkdir -p "$test_skill_dir"
  17. cat > "$test_skill_dir/SKILL.md" <<'EOF'
  18. ---
  19. name: test-skill
  20. description: A test skill for unit testing
  21. ---
  22. # Test Skill Content
  23. This is the content.
  24. EOF
  25. # Run Node.js test
  26. result=$(node --input-type=module <<'NODESCRIPT'
  27. import { extractFrontmatter } from '$HOME/.config/opencode/superpowers/lib/skills-core.js';
  28. const result = extractFrontmatter(process.env.TEST_HOME + '/test-skill/SKILL.md');
  29. console.log(JSON.stringify(result));
  30. NODESCRIPT
  31. ) 2>&1 || true
  32. # Try alternative approach if module import fails
  33. if ! echo "$result" | grep -q "test-skill"; then
  34. result=$(node -e "
  35. const path = require('path');
  36. const fs = require('fs');
  37. // Inline the extractFrontmatter function for testing
  38. function extractFrontmatter(filePath) {
  39. try {
  40. const content = fs.readFileSync(filePath, 'utf8');
  41. const lines = content.split('\n');
  42. let inFrontmatter = false;
  43. let name = '';
  44. let description = '';
  45. for (const line of lines) {
  46. if (line.trim() === '---') {
  47. if (inFrontmatter) break;
  48. inFrontmatter = true;
  49. continue;
  50. }
  51. if (inFrontmatter) {
  52. const match = line.match(/^(\w+):\s*(.*)$/);
  53. if (match) {
  54. const [, key, value] = match;
  55. if (key === 'name') name = value.trim();
  56. if (key === 'description') description = value.trim();
  57. }
  58. }
  59. }
  60. return { name, description };
  61. } catch (error) {
  62. return { name: '', description: '' };
  63. }
  64. }
  65. const result = extractFrontmatter('$TEST_HOME/test-skill/SKILL.md');
  66. console.log(JSON.stringify(result));
  67. " 2>&1)
  68. fi
  69. if echo "$result" | grep -q '"name":"test-skill"'; then
  70. echo " [PASS] extractFrontmatter parses name correctly"
  71. else
  72. echo " [FAIL] extractFrontmatter did not parse name"
  73. echo " Result: $result"
  74. exit 1
  75. fi
  76. if echo "$result" | grep -q '"description":"A test skill for unit testing"'; then
  77. echo " [PASS] extractFrontmatter parses description correctly"
  78. else
  79. echo " [FAIL] extractFrontmatter did not parse description"
  80. exit 1
  81. fi
  82. # Test 2: Test stripFrontmatter function
  83. echo ""
  84. echo "Test 2: Testing stripFrontmatter..."
  85. result=$(node -e "
  86. const fs = require('fs');
  87. function stripFrontmatter(content) {
  88. const lines = content.split('\n');
  89. let inFrontmatter = false;
  90. let frontmatterEnded = false;
  91. const contentLines = [];
  92. for (const line of lines) {
  93. if (line.trim() === '---') {
  94. if (inFrontmatter) {
  95. frontmatterEnded = true;
  96. continue;
  97. }
  98. inFrontmatter = true;
  99. continue;
  100. }
  101. if (frontmatterEnded || !inFrontmatter) {
  102. contentLines.push(line);
  103. }
  104. }
  105. return contentLines.join('\n').trim();
  106. }
  107. const content = fs.readFileSync('$TEST_HOME/test-skill/SKILL.md', 'utf8');
  108. const stripped = stripFrontmatter(content);
  109. console.log(stripped);
  110. " 2>&1)
  111. if echo "$result" | grep -q "# Test Skill Content"; then
  112. echo " [PASS] stripFrontmatter preserves content"
  113. else
  114. echo " [FAIL] stripFrontmatter did not preserve content"
  115. echo " Result: $result"
  116. exit 1
  117. fi
  118. if ! echo "$result" | grep -q "name: test-skill"; then
  119. echo " [PASS] stripFrontmatter removes frontmatter"
  120. else
  121. echo " [FAIL] stripFrontmatter did not remove frontmatter"
  122. exit 1
  123. fi
  124. # Test 3: Test findSkillsInDir function
  125. echo ""
  126. echo "Test 3: Testing findSkillsInDir..."
  127. # Create multiple test skills
  128. mkdir -p "$TEST_HOME/skills-dir/skill-a"
  129. mkdir -p "$TEST_HOME/skills-dir/skill-b"
  130. mkdir -p "$TEST_HOME/skills-dir/nested/skill-c"
  131. cat > "$TEST_HOME/skills-dir/skill-a/SKILL.md" <<'EOF'
  132. ---
  133. name: skill-a
  134. description: First skill
  135. ---
  136. # Skill A
  137. EOF
  138. cat > "$TEST_HOME/skills-dir/skill-b/SKILL.md" <<'EOF'
  139. ---
  140. name: skill-b
  141. description: Second skill
  142. ---
  143. # Skill B
  144. EOF
  145. cat > "$TEST_HOME/skills-dir/nested/skill-c/SKILL.md" <<'EOF'
  146. ---
  147. name: skill-c
  148. description: Nested skill
  149. ---
  150. # Skill C
  151. EOF
  152. result=$(node -e "
  153. const fs = require('fs');
  154. const path = require('path');
  155. function extractFrontmatter(filePath) {
  156. try {
  157. const content = fs.readFileSync(filePath, 'utf8');
  158. const lines = content.split('\n');
  159. let inFrontmatter = false;
  160. let name = '';
  161. let description = '';
  162. for (const line of lines) {
  163. if (line.trim() === '---') {
  164. if (inFrontmatter) break;
  165. inFrontmatter = true;
  166. continue;
  167. }
  168. if (inFrontmatter) {
  169. const match = line.match(/^(\w+):\s*(.*)$/);
  170. if (match) {
  171. const [, key, value] = match;
  172. if (key === 'name') name = value.trim();
  173. if (key === 'description') description = value.trim();
  174. }
  175. }
  176. }
  177. return { name, description };
  178. } catch (error) {
  179. return { name: '', description: '' };
  180. }
  181. }
  182. function findSkillsInDir(dir, sourceType, maxDepth = 3) {
  183. const skills = [];
  184. if (!fs.existsSync(dir)) return skills;
  185. function recurse(currentDir, depth) {
  186. if (depth > maxDepth) return;
  187. const entries = fs.readdirSync(currentDir, { withFileTypes: true });
  188. for (const entry of entries) {
  189. const fullPath = path.join(currentDir, entry.name);
  190. if (entry.isDirectory()) {
  191. const skillFile = path.join(fullPath, 'SKILL.md');
  192. if (fs.existsSync(skillFile)) {
  193. const { name, description } = extractFrontmatter(skillFile);
  194. skills.push({
  195. path: fullPath,
  196. skillFile: skillFile,
  197. name: name || entry.name,
  198. description: description || '',
  199. sourceType: sourceType
  200. });
  201. }
  202. recurse(fullPath, depth + 1);
  203. }
  204. }
  205. }
  206. recurse(dir, 0);
  207. return skills;
  208. }
  209. const skills = findSkillsInDir('$TEST_HOME/skills-dir', 'test', 3);
  210. console.log(JSON.stringify(skills, null, 2));
  211. " 2>&1)
  212. skill_count=$(echo "$result" | grep -c '"name":' || echo "0")
  213. if [ "$skill_count" -ge 3 ]; then
  214. echo " [PASS] findSkillsInDir found all skills (found $skill_count)"
  215. else
  216. echo " [FAIL] findSkillsInDir did not find all skills (expected 3, found $skill_count)"
  217. echo " Result: $result"
  218. exit 1
  219. fi
  220. if echo "$result" | grep -q '"name": "skill-c"'; then
  221. echo " [PASS] findSkillsInDir found nested skills"
  222. else
  223. echo " [FAIL] findSkillsInDir did not find nested skill"
  224. exit 1
  225. fi
  226. # Test 4: Test resolveSkillPath function
  227. echo ""
  228. echo "Test 4: Testing resolveSkillPath..."
  229. # Create skills in personal and superpowers locations for testing
  230. mkdir -p "$TEST_HOME/personal-skills/shared-skill"
  231. mkdir -p "$TEST_HOME/superpowers-skills/shared-skill"
  232. mkdir -p "$TEST_HOME/superpowers-skills/unique-skill"
  233. cat > "$TEST_HOME/personal-skills/shared-skill/SKILL.md" <<'EOF'
  234. ---
  235. name: shared-skill
  236. description: Personal version
  237. ---
  238. # Personal Shared
  239. EOF
  240. cat > "$TEST_HOME/superpowers-skills/shared-skill/SKILL.md" <<'EOF'
  241. ---
  242. name: shared-skill
  243. description: Superpowers version
  244. ---
  245. # Superpowers Shared
  246. EOF
  247. cat > "$TEST_HOME/superpowers-skills/unique-skill/SKILL.md" <<'EOF'
  248. ---
  249. name: unique-skill
  250. description: Only in superpowers
  251. ---
  252. # Unique
  253. EOF
  254. result=$(node -e "
  255. const fs = require('fs');
  256. const path = require('path');
  257. function resolveSkillPath(skillName, superpowersDir, personalDir) {
  258. const forceSuperpowers = skillName.startsWith('superpowers:');
  259. const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
  260. if (!forceSuperpowers && personalDir) {
  261. const personalPath = path.join(personalDir, actualSkillName);
  262. const personalSkillFile = path.join(personalPath, 'SKILL.md');
  263. if (fs.existsSync(personalSkillFile)) {
  264. return {
  265. skillFile: personalSkillFile,
  266. sourceType: 'personal',
  267. skillPath: actualSkillName
  268. };
  269. }
  270. }
  271. if (superpowersDir) {
  272. const superpowersPath = path.join(superpowersDir, actualSkillName);
  273. const superpowersSkillFile = path.join(superpowersPath, 'SKILL.md');
  274. if (fs.existsSync(superpowersSkillFile)) {
  275. return {
  276. skillFile: superpowersSkillFile,
  277. sourceType: 'superpowers',
  278. skillPath: actualSkillName
  279. };
  280. }
  281. }
  282. return null;
  283. }
  284. const superpowersDir = '$TEST_HOME/superpowers-skills';
  285. const personalDir = '$TEST_HOME/personal-skills';
  286. // Test 1: Shared skill should resolve to personal
  287. const shared = resolveSkillPath('shared-skill', superpowersDir, personalDir);
  288. console.log('SHARED:', JSON.stringify(shared));
  289. // Test 2: superpowers: prefix should force superpowers
  290. const forced = resolveSkillPath('superpowers:shared-skill', superpowersDir, personalDir);
  291. console.log('FORCED:', JSON.stringify(forced));
  292. // Test 3: Unique skill should resolve to superpowers
  293. const unique = resolveSkillPath('unique-skill', superpowersDir, personalDir);
  294. console.log('UNIQUE:', JSON.stringify(unique));
  295. // Test 4: Non-existent skill
  296. const notfound = resolveSkillPath('not-a-skill', superpowersDir, personalDir);
  297. console.log('NOTFOUND:', JSON.stringify(notfound));
  298. " 2>&1)
  299. if echo "$result" | grep -q 'SHARED:.*"sourceType":"personal"'; then
  300. echo " [PASS] Personal skills shadow superpowers skills"
  301. else
  302. echo " [FAIL] Personal skills not shadowing correctly"
  303. echo " Result: $result"
  304. exit 1
  305. fi
  306. if echo "$result" | grep -q 'FORCED:.*"sourceType":"superpowers"'; then
  307. echo " [PASS] superpowers: prefix forces superpowers resolution"
  308. else
  309. echo " [FAIL] superpowers: prefix not working"
  310. exit 1
  311. fi
  312. if echo "$result" | grep -q 'UNIQUE:.*"sourceType":"superpowers"'; then
  313. echo " [PASS] Unique superpowers skills are found"
  314. else
  315. echo " [FAIL] Unique superpowers skills not found"
  316. exit 1
  317. fi
  318. if echo "$result" | grep -q 'NOTFOUND: null'; then
  319. echo " [PASS] Non-existent skills return null"
  320. else
  321. echo " [FAIL] Non-existent skills should return null"
  322. exit 1
  323. fi
  324. echo ""
  325. echo "=== All skills-core library tests passed ==="