test-skills-core.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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 using inline function (avoids ESM path resolution issues in test env)
  26. result=$(node -e "
  27. const path = require('path');
  28. const fs = require('fs');
  29. // Inline the extractFrontmatter function for testing
  30. function extractFrontmatter(filePath) {
  31. try {
  32. const content = fs.readFileSync(filePath, 'utf8');
  33. const lines = content.split('\n');
  34. let inFrontmatter = false;
  35. let name = '';
  36. let description = '';
  37. for (const line of lines) {
  38. if (line.trim() === '---') {
  39. if (inFrontmatter) break;
  40. inFrontmatter = true;
  41. continue;
  42. }
  43. if (inFrontmatter) {
  44. const match = line.match(/^(\w+):\s*(.*)$/);
  45. if (match) {
  46. const [, key, value] = match;
  47. if (key === 'name') name = value.trim();
  48. if (key === 'description') description = value.trim();
  49. }
  50. }
  51. }
  52. return { name, description };
  53. } catch (error) {
  54. return { name: '', description: '' };
  55. }
  56. }
  57. const result = extractFrontmatter('$TEST_HOME/test-skill/SKILL.md');
  58. console.log(JSON.stringify(result));
  59. " 2>&1)
  60. if echo "$result" | grep -q '"name":"test-skill"'; then
  61. echo " [PASS] extractFrontmatter parses name correctly"
  62. else
  63. echo " [FAIL] extractFrontmatter did not parse name"
  64. echo " Result: $result"
  65. exit 1
  66. fi
  67. if echo "$result" | grep -q '"description":"A test skill for unit testing"'; then
  68. echo " [PASS] extractFrontmatter parses description correctly"
  69. else
  70. echo " [FAIL] extractFrontmatter did not parse description"
  71. exit 1
  72. fi
  73. # Test 2: Test stripFrontmatter function
  74. echo ""
  75. echo "Test 2: Testing stripFrontmatter..."
  76. result=$(node -e "
  77. const fs = require('fs');
  78. function stripFrontmatter(content) {
  79. const lines = content.split('\n');
  80. let inFrontmatter = false;
  81. let frontmatterEnded = false;
  82. const contentLines = [];
  83. for (const line of lines) {
  84. if (line.trim() === '---') {
  85. if (inFrontmatter) {
  86. frontmatterEnded = true;
  87. continue;
  88. }
  89. inFrontmatter = true;
  90. continue;
  91. }
  92. if (frontmatterEnded || !inFrontmatter) {
  93. contentLines.push(line);
  94. }
  95. }
  96. return contentLines.join('\n').trim();
  97. }
  98. const content = fs.readFileSync('$TEST_HOME/test-skill/SKILL.md', 'utf8');
  99. const stripped = stripFrontmatter(content);
  100. console.log(stripped);
  101. " 2>&1)
  102. if echo "$result" | grep -q "# Test Skill Content"; then
  103. echo " [PASS] stripFrontmatter preserves content"
  104. else
  105. echo " [FAIL] stripFrontmatter did not preserve content"
  106. echo " Result: $result"
  107. exit 1
  108. fi
  109. if ! echo "$result" | grep -q "name: test-skill"; then
  110. echo " [PASS] stripFrontmatter removes frontmatter"
  111. else
  112. echo " [FAIL] stripFrontmatter did not remove frontmatter"
  113. exit 1
  114. fi
  115. # Test 3: Test findSkillsInDir function
  116. echo ""
  117. echo "Test 3: Testing findSkillsInDir..."
  118. # Create multiple test skills
  119. mkdir -p "$TEST_HOME/skills-dir/skill-a"
  120. mkdir -p "$TEST_HOME/skills-dir/skill-b"
  121. mkdir -p "$TEST_HOME/skills-dir/nested/skill-c"
  122. cat > "$TEST_HOME/skills-dir/skill-a/SKILL.md" <<'EOF'
  123. ---
  124. name: skill-a
  125. description: First skill
  126. ---
  127. # Skill A
  128. EOF
  129. cat > "$TEST_HOME/skills-dir/skill-b/SKILL.md" <<'EOF'
  130. ---
  131. name: skill-b
  132. description: Second skill
  133. ---
  134. # Skill B
  135. EOF
  136. cat > "$TEST_HOME/skills-dir/nested/skill-c/SKILL.md" <<'EOF'
  137. ---
  138. name: skill-c
  139. description: Nested skill
  140. ---
  141. # Skill C
  142. EOF
  143. result=$(node -e "
  144. const fs = require('fs');
  145. const path = require('path');
  146. function extractFrontmatter(filePath) {
  147. try {
  148. const content = fs.readFileSync(filePath, 'utf8');
  149. const lines = content.split('\n');
  150. let inFrontmatter = false;
  151. let name = '';
  152. let description = '';
  153. for (const line of lines) {
  154. if (line.trim() === '---') {
  155. if (inFrontmatter) break;
  156. inFrontmatter = true;
  157. continue;
  158. }
  159. if (inFrontmatter) {
  160. const match = line.match(/^(\w+):\s*(.*)$/);
  161. if (match) {
  162. const [, key, value] = match;
  163. if (key === 'name') name = value.trim();
  164. if (key === 'description') description = value.trim();
  165. }
  166. }
  167. }
  168. return { name, description };
  169. } catch (error) {
  170. return { name: '', description: '' };
  171. }
  172. }
  173. function findSkillsInDir(dir, sourceType, maxDepth = 3) {
  174. const skills = [];
  175. if (!fs.existsSync(dir)) return skills;
  176. function recurse(currentDir, depth) {
  177. if (depth > maxDepth) return;
  178. const entries = fs.readdirSync(currentDir, { withFileTypes: true });
  179. for (const entry of entries) {
  180. const fullPath = path.join(currentDir, entry.name);
  181. if (entry.isDirectory()) {
  182. const skillFile = path.join(fullPath, 'SKILL.md');
  183. if (fs.existsSync(skillFile)) {
  184. const { name, description } = extractFrontmatter(skillFile);
  185. skills.push({
  186. path: fullPath,
  187. skillFile: skillFile,
  188. name: name || entry.name,
  189. description: description || '',
  190. sourceType: sourceType
  191. });
  192. }
  193. recurse(fullPath, depth + 1);
  194. }
  195. }
  196. }
  197. recurse(dir, 0);
  198. return skills;
  199. }
  200. const skills = findSkillsInDir('$TEST_HOME/skills-dir', 'test', 3);
  201. console.log(JSON.stringify(skills, null, 2));
  202. " 2>&1)
  203. skill_count=$(echo "$result" | grep -c '"name":' || echo "0")
  204. if [ "$skill_count" -ge 3 ]; then
  205. echo " [PASS] findSkillsInDir found all skills (found $skill_count)"
  206. else
  207. echo " [FAIL] findSkillsInDir did not find all skills (expected 3, found $skill_count)"
  208. echo " Result: $result"
  209. exit 1
  210. fi
  211. if echo "$result" | grep -q '"name": "skill-c"'; then
  212. echo " [PASS] findSkillsInDir found nested skills"
  213. else
  214. echo " [FAIL] findSkillsInDir did not find nested skill"
  215. exit 1
  216. fi
  217. # Test 4: Test resolveSkillPath function
  218. echo ""
  219. echo "Test 4: Testing resolveSkillPath..."
  220. # Create skills in personal and superpowers locations for testing
  221. mkdir -p "$TEST_HOME/personal-skills/shared-skill"
  222. mkdir -p "$TEST_HOME/superpowers-skills/shared-skill"
  223. mkdir -p "$TEST_HOME/superpowers-skills/unique-skill"
  224. cat > "$TEST_HOME/personal-skills/shared-skill/SKILL.md" <<'EOF'
  225. ---
  226. name: shared-skill
  227. description: Personal version
  228. ---
  229. # Personal Shared
  230. EOF
  231. cat > "$TEST_HOME/superpowers-skills/shared-skill/SKILL.md" <<'EOF'
  232. ---
  233. name: shared-skill
  234. description: Superpowers version
  235. ---
  236. # Superpowers Shared
  237. EOF
  238. cat > "$TEST_HOME/superpowers-skills/unique-skill/SKILL.md" <<'EOF'
  239. ---
  240. name: unique-skill
  241. description: Only in superpowers
  242. ---
  243. # Unique
  244. EOF
  245. result=$(node -e "
  246. const fs = require('fs');
  247. const path = require('path');
  248. function resolveSkillPath(skillName, superpowersDir, personalDir) {
  249. const forceSuperpowers = skillName.startsWith('superpowers:');
  250. const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
  251. if (!forceSuperpowers && personalDir) {
  252. const personalPath = path.join(personalDir, actualSkillName);
  253. const personalSkillFile = path.join(personalPath, 'SKILL.md');
  254. if (fs.existsSync(personalSkillFile)) {
  255. return {
  256. skillFile: personalSkillFile,
  257. sourceType: 'personal',
  258. skillPath: actualSkillName
  259. };
  260. }
  261. }
  262. if (superpowersDir) {
  263. const superpowersPath = path.join(superpowersDir, actualSkillName);
  264. const superpowersSkillFile = path.join(superpowersPath, 'SKILL.md');
  265. if (fs.existsSync(superpowersSkillFile)) {
  266. return {
  267. skillFile: superpowersSkillFile,
  268. sourceType: 'superpowers',
  269. skillPath: actualSkillName
  270. };
  271. }
  272. }
  273. return null;
  274. }
  275. const superpowersDir = '$TEST_HOME/superpowers-skills';
  276. const personalDir = '$TEST_HOME/personal-skills';
  277. // Test 1: Shared skill should resolve to personal
  278. const shared = resolveSkillPath('shared-skill', superpowersDir, personalDir);
  279. console.log('SHARED:', JSON.stringify(shared));
  280. // Test 2: superpowers: prefix should force superpowers
  281. const forced = resolveSkillPath('superpowers:shared-skill', superpowersDir, personalDir);
  282. console.log('FORCED:', JSON.stringify(forced));
  283. // Test 3: Unique skill should resolve to superpowers
  284. const unique = resolveSkillPath('unique-skill', superpowersDir, personalDir);
  285. console.log('UNIQUE:', JSON.stringify(unique));
  286. // Test 4: Non-existent skill
  287. const notfound = resolveSkillPath('not-a-skill', superpowersDir, personalDir);
  288. console.log('NOTFOUND:', JSON.stringify(notfound));
  289. " 2>&1)
  290. if echo "$result" | grep -q 'SHARED:.*"sourceType":"personal"'; then
  291. echo " [PASS] Personal skills shadow superpowers skills"
  292. else
  293. echo " [FAIL] Personal skills not shadowing correctly"
  294. echo " Result: $result"
  295. exit 1
  296. fi
  297. if echo "$result" | grep -q 'FORCED:.*"sourceType":"superpowers"'; then
  298. echo " [PASS] superpowers: prefix forces superpowers resolution"
  299. else
  300. echo " [FAIL] superpowers: prefix not working"
  301. exit 1
  302. fi
  303. if echo "$result" | grep -q 'UNIQUE:.*"sourceType":"superpowers"'; then
  304. echo " [PASS] Unique superpowers skills are found"
  305. else
  306. echo " [FAIL] Unique superpowers skills not found"
  307. exit 1
  308. fi
  309. if echo "$result" | grep -q 'NOTFOUND: null'; then
  310. echo " [PASS] Non-existent skills return null"
  311. else
  312. echo " [FAIL] Non-existent skills should return null"
  313. exit 1
  314. fi
  315. # Test 5: Test checkForUpdates function
  316. echo ""
  317. echo "Test 5: Testing checkForUpdates..."
  318. # Create a test git repo
  319. mkdir -p "$TEST_HOME/test-repo"
  320. cd "$TEST_HOME/test-repo"
  321. git init --quiet
  322. git config user.email "test@test.com"
  323. git config user.name "Test"
  324. echo "test" > file.txt
  325. git add file.txt
  326. git commit -m "initial" --quiet
  327. cd "$SCRIPT_DIR"
  328. # Test checkForUpdates on repo without remote (should return false, not error)
  329. result=$(node -e "
  330. const { execSync } = require('child_process');
  331. function checkForUpdates(repoDir) {
  332. try {
  333. const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
  334. cwd: repoDir,
  335. timeout: 3000,
  336. encoding: 'utf8',
  337. stdio: 'pipe'
  338. });
  339. const statusLines = output.split('\n');
  340. for (const line of statusLines) {
  341. if (line.startsWith('## ') && line.includes('[behind ')) {
  342. return true;
  343. }
  344. }
  345. return false;
  346. } catch (error) {
  347. return false;
  348. }
  349. }
  350. // Test 1: Repo without remote should return false (graceful error handling)
  351. const result1 = checkForUpdates('$TEST_HOME/test-repo');
  352. console.log('NO_REMOTE:', result1);
  353. // Test 2: Non-existent directory should return false
  354. const result2 = checkForUpdates('$TEST_HOME/nonexistent');
  355. console.log('NONEXISTENT:', result2);
  356. // Test 3: Non-git directory should return false
  357. const result3 = checkForUpdates('$TEST_HOME');
  358. console.log('NOT_GIT:', result3);
  359. " 2>&1)
  360. if echo "$result" | grep -q 'NO_REMOTE: false'; then
  361. echo " [PASS] checkForUpdates handles repo without remote gracefully"
  362. else
  363. echo " [FAIL] checkForUpdates should return false for repo without remote"
  364. echo " Result: $result"
  365. exit 1
  366. fi
  367. if echo "$result" | grep -q 'NONEXISTENT: false'; then
  368. echo " [PASS] checkForUpdates handles non-existent directory"
  369. else
  370. echo " [FAIL] checkForUpdates should return false for non-existent directory"
  371. exit 1
  372. fi
  373. if echo "$result" | grep -q 'NOT_GIT: false'; then
  374. echo " [PASS] checkForUpdates handles non-git directory"
  375. else
  376. echo " [FAIL] checkForUpdates should return false for non-git directory"
  377. exit 1
  378. fi
  379. echo ""
  380. echo "=== All skills-core library tests passed ==="