| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- #!/usr/bin/env bash
- # Test: Skills Core Library
- # Tests the skills-core.js library functions directly via Node.js
- # Does not require OpenCode - tests pure library functionality
- set -euo pipefail
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
- echo "=== Test: Skills Core Library ==="
- # Source setup to create isolated environment
- source "$SCRIPT_DIR/setup.sh"
- # Trap to cleanup on exit
- trap cleanup_test_env EXIT
- # Test 1: Test extractFrontmatter function
- echo "Test 1: Testing extractFrontmatter..."
- # Create test file with frontmatter
- test_skill_dir="$TEST_HOME/test-skill"
- mkdir -p "$test_skill_dir"
- cat > "$test_skill_dir/SKILL.md" <<'EOF'
- ---
- name: test-skill
- description: A test skill for unit testing
- ---
- # Test Skill Content
- This is the content.
- EOF
- # Run Node.js test using inline function (avoids ESM path resolution issues in test env)
- result=$(node -e "
- const path = require('path');
- const fs = require('fs');
- // Inline the extractFrontmatter function for testing
- function extractFrontmatter(filePath) {
- try {
- const content = fs.readFileSync(filePath, 'utf8');
- const lines = content.split('\n');
- let inFrontmatter = false;
- let name = '';
- let description = '';
- for (const line of lines) {
- if (line.trim() === '---') {
- if (inFrontmatter) break;
- inFrontmatter = true;
- continue;
- }
- if (inFrontmatter) {
- const match = line.match(/^(\w+):\s*(.*)$/);
- if (match) {
- const [, key, value] = match;
- if (key === 'name') name = value.trim();
- if (key === 'description') description = value.trim();
- }
- }
- }
- return { name, description };
- } catch (error) {
- return { name: '', description: '' };
- }
- }
- const result = extractFrontmatter('$TEST_HOME/test-skill/SKILL.md');
- console.log(JSON.stringify(result));
- " 2>&1)
- if echo "$result" | grep -q '"name":"test-skill"'; then
- echo " [PASS] extractFrontmatter parses name correctly"
- else
- echo " [FAIL] extractFrontmatter did not parse name"
- echo " Result: $result"
- exit 1
- fi
- if echo "$result" | grep -q '"description":"A test skill for unit testing"'; then
- echo " [PASS] extractFrontmatter parses description correctly"
- else
- echo " [FAIL] extractFrontmatter did not parse description"
- exit 1
- fi
- # Test 2: Test stripFrontmatter function
- echo ""
- echo "Test 2: Testing stripFrontmatter..."
- result=$(node -e "
- const fs = require('fs');
- function stripFrontmatter(content) {
- const lines = content.split('\n');
- let inFrontmatter = false;
- let frontmatterEnded = false;
- const contentLines = [];
- for (const line of lines) {
- if (line.trim() === '---') {
- if (inFrontmatter) {
- frontmatterEnded = true;
- continue;
- }
- inFrontmatter = true;
- continue;
- }
- if (frontmatterEnded || !inFrontmatter) {
- contentLines.push(line);
- }
- }
- return contentLines.join('\n').trim();
- }
- const content = fs.readFileSync('$TEST_HOME/test-skill/SKILL.md', 'utf8');
- const stripped = stripFrontmatter(content);
- console.log(stripped);
- " 2>&1)
- if echo "$result" | grep -q "# Test Skill Content"; then
- echo " [PASS] stripFrontmatter preserves content"
- else
- echo " [FAIL] stripFrontmatter did not preserve content"
- echo " Result: $result"
- exit 1
- fi
- if ! echo "$result" | grep -q "name: test-skill"; then
- echo " [PASS] stripFrontmatter removes frontmatter"
- else
- echo " [FAIL] stripFrontmatter did not remove frontmatter"
- exit 1
- fi
- # Test 3: Test findSkillsInDir function
- echo ""
- echo "Test 3: Testing findSkillsInDir..."
- # Create multiple test skills
- mkdir -p "$TEST_HOME/skills-dir/skill-a"
- mkdir -p "$TEST_HOME/skills-dir/skill-b"
- mkdir -p "$TEST_HOME/skills-dir/nested/skill-c"
- cat > "$TEST_HOME/skills-dir/skill-a/SKILL.md" <<'EOF'
- ---
- name: skill-a
- description: First skill
- ---
- # Skill A
- EOF
- cat > "$TEST_HOME/skills-dir/skill-b/SKILL.md" <<'EOF'
- ---
- name: skill-b
- description: Second skill
- ---
- # Skill B
- EOF
- cat > "$TEST_HOME/skills-dir/nested/skill-c/SKILL.md" <<'EOF'
- ---
- name: skill-c
- description: Nested skill
- ---
- # Skill C
- EOF
- result=$(node -e "
- const fs = require('fs');
- const path = require('path');
- function extractFrontmatter(filePath) {
- try {
- const content = fs.readFileSync(filePath, 'utf8');
- const lines = content.split('\n');
- let inFrontmatter = false;
- let name = '';
- let description = '';
- for (const line of lines) {
- if (line.trim() === '---') {
- if (inFrontmatter) break;
- inFrontmatter = true;
- continue;
- }
- if (inFrontmatter) {
- const match = line.match(/^(\w+):\s*(.*)$/);
- if (match) {
- const [, key, value] = match;
- if (key === 'name') name = value.trim();
- if (key === 'description') description = value.trim();
- }
- }
- }
- return { name, description };
- } catch (error) {
- return { name: '', description: '' };
- }
- }
- function findSkillsInDir(dir, sourceType, maxDepth = 3) {
- const skills = [];
- if (!fs.existsSync(dir)) return skills;
- function recurse(currentDir, depth) {
- if (depth > maxDepth) return;
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
- for (const entry of entries) {
- const fullPath = path.join(currentDir, entry.name);
- if (entry.isDirectory()) {
- const skillFile = path.join(fullPath, 'SKILL.md');
- if (fs.existsSync(skillFile)) {
- const { name, description } = extractFrontmatter(skillFile);
- skills.push({
- path: fullPath,
- skillFile: skillFile,
- name: name || entry.name,
- description: description || '',
- sourceType: sourceType
- });
- }
- recurse(fullPath, depth + 1);
- }
- }
- }
- recurse(dir, 0);
- return skills;
- }
- const skills = findSkillsInDir('$TEST_HOME/skills-dir', 'test', 3);
- console.log(JSON.stringify(skills, null, 2));
- " 2>&1)
- skill_count=$(echo "$result" | grep -c '"name":' || echo "0")
- if [ "$skill_count" -ge 3 ]; then
- echo " [PASS] findSkillsInDir found all skills (found $skill_count)"
- else
- echo " [FAIL] findSkillsInDir did not find all skills (expected 3, found $skill_count)"
- echo " Result: $result"
- exit 1
- fi
- if echo "$result" | grep -q '"name": "skill-c"'; then
- echo " [PASS] findSkillsInDir found nested skills"
- else
- echo " [FAIL] findSkillsInDir did not find nested skill"
- exit 1
- fi
- # Test 4: Test resolveSkillPath function
- echo ""
- echo "Test 4: Testing resolveSkillPath..."
- # Create skills in personal and superpowers locations for testing
- mkdir -p "$TEST_HOME/personal-skills/shared-skill"
- mkdir -p "$TEST_HOME/superpowers-skills/shared-skill"
- mkdir -p "$TEST_HOME/superpowers-skills/unique-skill"
- cat > "$TEST_HOME/personal-skills/shared-skill/SKILL.md" <<'EOF'
- ---
- name: shared-skill
- description: Personal version
- ---
- # Personal Shared
- EOF
- cat > "$TEST_HOME/superpowers-skills/shared-skill/SKILL.md" <<'EOF'
- ---
- name: shared-skill
- description: Superpowers version
- ---
- # Superpowers Shared
- EOF
- cat > "$TEST_HOME/superpowers-skills/unique-skill/SKILL.md" <<'EOF'
- ---
- name: unique-skill
- description: Only in superpowers
- ---
- # Unique
- EOF
- result=$(node -e "
- const fs = require('fs');
- const path = require('path');
- function resolveSkillPath(skillName, superpowersDir, personalDir) {
- const forceSuperpowers = skillName.startsWith('superpowers:');
- const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
- if (!forceSuperpowers && personalDir) {
- const personalPath = path.join(personalDir, actualSkillName);
- const personalSkillFile = path.join(personalPath, 'SKILL.md');
- if (fs.existsSync(personalSkillFile)) {
- return {
- skillFile: personalSkillFile,
- sourceType: 'personal',
- skillPath: actualSkillName
- };
- }
- }
- if (superpowersDir) {
- const superpowersPath = path.join(superpowersDir, actualSkillName);
- const superpowersSkillFile = path.join(superpowersPath, 'SKILL.md');
- if (fs.existsSync(superpowersSkillFile)) {
- return {
- skillFile: superpowersSkillFile,
- sourceType: 'superpowers',
- skillPath: actualSkillName
- };
- }
- }
- return null;
- }
- const superpowersDir = '$TEST_HOME/superpowers-skills';
- const personalDir = '$TEST_HOME/personal-skills';
- // Test 1: Shared skill should resolve to personal
- const shared = resolveSkillPath('shared-skill', superpowersDir, personalDir);
- console.log('SHARED:', JSON.stringify(shared));
- // Test 2: superpowers: prefix should force superpowers
- const forced = resolveSkillPath('superpowers:shared-skill', superpowersDir, personalDir);
- console.log('FORCED:', JSON.stringify(forced));
- // Test 3: Unique skill should resolve to superpowers
- const unique = resolveSkillPath('unique-skill', superpowersDir, personalDir);
- console.log('UNIQUE:', JSON.stringify(unique));
- // Test 4: Non-existent skill
- const notfound = resolveSkillPath('not-a-skill', superpowersDir, personalDir);
- console.log('NOTFOUND:', JSON.stringify(notfound));
- " 2>&1)
- if echo "$result" | grep -q 'SHARED:.*"sourceType":"personal"'; then
- echo " [PASS] Personal skills shadow superpowers skills"
- else
- echo " [FAIL] Personal skills not shadowing correctly"
- echo " Result: $result"
- exit 1
- fi
- if echo "$result" | grep -q 'FORCED:.*"sourceType":"superpowers"'; then
- echo " [PASS] superpowers: prefix forces superpowers resolution"
- else
- echo " [FAIL] superpowers: prefix not working"
- exit 1
- fi
- if echo "$result" | grep -q 'UNIQUE:.*"sourceType":"superpowers"'; then
- echo " [PASS] Unique superpowers skills are found"
- else
- echo " [FAIL] Unique superpowers skills not found"
- exit 1
- fi
- if echo "$result" | grep -q 'NOTFOUND: null'; then
- echo " [PASS] Non-existent skills return null"
- else
- echo " [FAIL] Non-existent skills should return null"
- exit 1
- fi
- # Test 5: Test checkForUpdates function
- echo ""
- echo "Test 5: Testing checkForUpdates..."
- # Create a test git repo
- mkdir -p "$TEST_HOME/test-repo"
- cd "$TEST_HOME/test-repo"
- git init --quiet
- git config user.email "test@test.com"
- git config user.name "Test"
- echo "test" > file.txt
- git add file.txt
- git commit -m "initial" --quiet
- cd "$SCRIPT_DIR"
- # Test checkForUpdates on repo without remote (should return false, not error)
- result=$(node -e "
- const { execSync } = require('child_process');
- function checkForUpdates(repoDir) {
- try {
- const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
- cwd: repoDir,
- timeout: 3000,
- encoding: 'utf8',
- stdio: 'pipe'
- });
- const statusLines = output.split('\n');
- for (const line of statusLines) {
- if (line.startsWith('## ') && line.includes('[behind ')) {
- return true;
- }
- }
- return false;
- } catch (error) {
- return false;
- }
- }
- // Test 1: Repo without remote should return false (graceful error handling)
- const result1 = checkForUpdates('$TEST_HOME/test-repo');
- console.log('NO_REMOTE:', result1);
- // Test 2: Non-existent directory should return false
- const result2 = checkForUpdates('$TEST_HOME/nonexistent');
- console.log('NONEXISTENT:', result2);
- // Test 3: Non-git directory should return false
- const result3 = checkForUpdates('$TEST_HOME');
- console.log('NOT_GIT:', result3);
- " 2>&1)
- if echo "$result" | grep -q 'NO_REMOTE: false'; then
- echo " [PASS] checkForUpdates handles repo without remote gracefully"
- else
- echo " [FAIL] checkForUpdates should return false for repo without remote"
- echo " Result: $result"
- exit 1
- fi
- if echo "$result" | grep -q 'NONEXISTENT: false'; then
- echo " [PASS] checkForUpdates handles non-existent directory"
- else
- echo " [FAIL] checkForUpdates should return false for non-existent directory"
- exit 1
- fi
- if echo "$result" | grep -q 'NOT_GIT: false'; then
- echo " [PASS] checkForUpdates handles non-git directory"
- else
- echo " [FAIL] checkForUpdates should return false for non-git directory"
- exit 1
- fi
- echo ""
- echo "=== All skills-core library tests passed ==="
|